هام: يتعلق هذا بتشغيل JavaScript وTypeScript فقط. ومع ذلك، قد تكون الكتابة أيضًا هي الاتجاه لتشغيل تعليمات برمجية أخرى بلغات أخرى.
السماح للمستخدمين بتنفيذ التعليمات البرمجية الخاصة بهم داخل التطبيق الخاص بك يفتح عالمًا من التخصيص والوظائف، ومع ذلك فإنه يعرض النظام الأساسي الخاص بك أيضًا لتهديدات أمنية كبيرة.
نظرًا لأنه رمز user، فكل شيء متوقع، بدءًا من إيقاف الخوادم (قد يكون ذلك عبارة عن حلقات لا نهاية لها) وحتى سرقة المعلومات الحساسة.
سوف تستكشف هذه المقالة استراتيجيات مختلفة للتخفيف من تشغيل تعليمات برمجية للمستخدم، بما في ذلك عمال الويب، وتحليل التعليمات البرمجية الثابتة، والمزيد...
هناك العديد من السيناريوهات التي تحتاج فيها إلى تشغيل التعليمات البرمجية المقدمة من المستخدم، بدءًا من بيئات التطوير التعاونية مثل CodeSandbox وStackBiltz إلى منصات واجهة برمجة التطبيقات القابلة للتخصيص مثل يناير. حتى ملاعب الكود معرضة للمخاطر.
أي أن المزايا الأساسية لتشغيل التعليمات البرمجية المقدمة من المستخدم بأمان هي:
لا يعد تشغيل رمز المستخدم ضارًا حتى تشعر بالقلق من أن هذا قد يعرض بعض البيانات للسرقة. مهما كانت البيانات التي تشعر بالقلق بشأنها، فسيتم اعتبارها معلومات حساسة. على سبيل المثال، في معظم الحالات، تعد JWT معلومات حساسة (ربما عند استخدامها كآلية مصادقة)
ضع في اعتبارك المخاطر المحتملة لـ JWT المخزنة في ملفات تعريف الارتباط المرسلة مع كل طلب. يمكن للمستخدم أن يقوم عن غير قصد بتشغيل طلب يرسل JWT إلى خادم ضار، و...
الأبسط على الإطلاق، ولكنه الأكثر خطورة.
eval('console.log("I am dangerous!")');
عند تشغيل هذا الرمز، فإنه يسجل تلك الرسالة. في الأساس، eval هو مترجم JS قادر على الوصول إلى النطاق العام/النافذة.
const res = await eval('fetch(`https://jsonplaceholder.typicode.com/users`)'); const users = await res.json();
يستخدم هذا الرمز الجلب المحدد في النطاق العالمي. المترجم لا يعرف ذلك، ولكن بما أن eval يمكنه الوصول إلى النافذة، فهو يعرف ذلك. وهذا يعني أن تشغيل التقييم في المتصفح يختلف عن تشغيله في بيئة خادم أو عامل.
eval(`document.body`);
وماذا عن هذا...
eval(`while (true) {}`);
سيوقف هذا الرمز علامة تبويب المتصفح. قد تتساءل لماذا يفعل المستخدم ذلك بنفسه. حسنًا، ربما يقومون بنسخ التعليمات البرمجية من الإنترنت. ولهذا السبب يفضل إجراء تحليل ثابت مع/أو تحديد مربع زمني للتنفيذ.
قد ترغب في التحقق من مستندات MDN حول التقييم
يمكن تنفيذ المربع الزمني عن طريق تشغيل التعليمات البرمجية في عامل الويب واستخدام setTimeout لتحديد وقت التنفيذ.
async function timebox(code, timeout = 5000) { const worker = new Worker('user-runner-worker.js'); worker.postMessage(code); const timerId = setTimeout(() => { worker.terminate(); reject(new Error('Code execution timed out')); }, timeout); return new Promise((resolve, reject) => { worker.onmessage = event => { clearTimeout(timerId); resolve(event.data); }; worker.onerror = error => { clearTimeout(timerId); reject(error); }; }); } await timebox('while (true) {}');
هذا مشابه للتقييم ولكنه أكثر أمانًا لأنه لا يمكنه الوصول إلى النطاق المرفق.
const userFunction = new Function('param', 'console.log(param);'); userFunction(2);
سيسجل هذا الرمز 2.
ملاحظة: الوسيطة الثانية هي نص الوظيفة.
لا يمكن لمنشئ الوظيفة الوصول إلى النطاق المتضمن، لذا فإن التعليمة البرمجية التالية ستؤدي إلى خطأ.
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
ولكن يمكنه الوصول إلى النطاق العالمي حتى يعمل مثال الجلب من الأعلى.
يمكنك تشغيل "Function Constructor والتقييم على WebWorker، وهو أكثر أمانًا نظرًا لعدم وجود وصول إلى DOM.
لوضع المزيد من القيود، فكر في عدم السماح باستخدام الكائنات العامة مثل الجلب، XMLHttpRequest، sendBeacon تحقق من هذه الكتابة حول كيفية القيام بذلك.
Isolated-VM هي مكتبة تسمح لك بتشغيل التعليمات البرمجية في جهاز افتراضي منفصل (واجهة v8 المعزولة)
import ivm from 'isolated-vm'; const code = `count = 5;`; const isolate = new ivm.Isolate({ memoryLimit: 32 /* MB */ }); const script = isolate.compileScriptSync(code); const context = isolate.createContextSync(); const jail = context.global; jail.setSync('log', console.log); context.evalSync('log("hello world")');
هذا الرمز سوف يسجل مرحبا بالعالم
يعد هذا خيارًا مثيرًا لأنه يوفر بيئة محمية لتشغيل التعليمات البرمجية. أحد التحذيرات هو أنك تحتاج إلى بيئة تحتوي على روابط Javascript. ومع ذلك، هناك مشروع مثير للاهتمام يسمى Extism يسهل ذلك. قد ترغب في متابعة البرنامج التعليمي الخاص بهم.
الأمر المذهل في الأمر هو أنك ستستخدم eval لتشغيل التعليمات البرمجية، ولكن نظرًا لطبيعة WebAssembly، فإن DOM والشبكة ونظام الملفات والوصول إلى البيئة المضيفة غير ممكن (على الرغم من أنها قد تختلف بناءً على وقت تشغيل Wasm).
function evaluate() { const { code, input } = JSON.parse(Host.inputString()); const func = eval(code); const result = func(input).toString(); Host.outputString(result); } module.exports = { evaluate };
سيتعين عليك تجميع الكود أعلاه أولاً باستخدام Extism، والذي سيخرج ملف Wasm الذي يمكن تشغيله في بيئة بها وقت تشغيل Wasm (المتصفح أو العقدة js).
const message = { input: '1,2,3,4,5', code: ` const sum = (str) => str .split(',') .reduce((acc, curr) => acc parseInt(curr), 0); module.exports = sum; `, }; // continue running the wasm file
ننتقل الآن إلى جانب الخادم، حيث يعد Docker خيارًا رائعًا لتشغيل التعليمات البرمجية بمعزل عن الجهاز المضيف. (احذر من هروب الحاويات)
يمكنك استخدام dockerode لتشغيل التعليمات البرمجية في حاوية.
import Docker from 'dockerode'; const docker = new Docker(); const code = `console.log("hello world")`; const container = await docker.createContainer({ Image: 'node:lts', Cmd: ['node', '-e', code], User: 'node', WorkingDir: '/app', AttachStdout: true, AttachStderr: true, OpenStdin: false, AttachStdin: false, Tty: true, NetworkDisabled: true, HostConfig: { AutoRemove: true, ReadonlyPaths: ['/'], ReadonlyRootfs: true, CapDrop: ['ALL'], Memory: 8 * 1024 * 1024, SecurityOpt: ['no-new-privileges'], }, });
ضع في اعتبارك أنك بحاجة إلى التأكد من تثبيت عامل الإرساء وتشغيله على الخادم. أوصي بوجود خادم منفصل مخصص لهذا فقط والذي يعمل كخادم ذو وظيفة خالصة.
علاوة على ذلك، قد تستفيد من إلقاء نظرة على sysbox، وهو عبارة عن حاوية تشبه VM توفر بيئة أكثر أمانًا. يستحق Sysbox ذلك، خاصة إذا كان التطبيق الرئيسي يعمل في حاوية، مما يعني أنك ستقوم بتشغيل Docker في Docker.
كانت هذه هي الطريقة المفضلة في شهر يناير، ولكن بعد فترة وجيزة، تطلبت القدرات اللغوية أكثر من مجرد تمرير التعليمات البرمجية عبر غلاف الحاوية. بالإضافة إلى ذلك، لسبب ما، ترتفع ذاكرة الخادم بشكل متكرر؛ نقوم بتشغيل الكود داخل حاويات قابلة للإزالة ذاتيًا عند كل ضغطة مفتاح مرتجعة لمدة ثانية واحدة. (يمكنك القيام به على نحو أفضل!)
أنا معجب بشكل خاص بـ Firecracker، ولكن إعدادها يتطلب القليل من العمل، لذلك إذا لم تتمكن من توفير الوقت بعد، فأنت تريد أن تكون في الجانب الآمن، وقم بإجراء مزيج من التحليل الثابت وتنفيذ ملاكمة الوقت . يمكنك استخدام esprima لتحليل الكود والتحقق من أي عمل ضار.
حسنًا، نفس القصة مع خطوة إضافية واحدة (قد تكون اختيارية): قم بنقل الكود إلى JavaScript قبل تشغيله. ببساطة، يمكنك استخدام برنامج التحويل البرمجي esbuild أو typescript، ثم متابعة الطرق المذكورة أعلاه.
async function build(userCode: string) { const result = await esbuild.build({ stdin: { contents: `${userCode}`, loader: 'ts', resolveDir: __dirname, }, inject: [ // In case you want to inject some code ], platform: 'node', write: false, treeShaking: false, sourcemap: false, minify: false, drop: ['debugger', 'console'], keepNames: true, format: 'cjs', bundle: true, target: 'es2022', plugins: [ nodeExternalsPlugin(), // make all the non-native modules external ], }); return result.outputFiles![0].text; }
ملحوظات:
بالإضافة إلى ذلك، يمكنك تجنب النقل تمامًا عن طريق تشغيل التعليمات البرمجية باستخدام Deno أو Bun في حاوية عامل إرساء نظرًا لأنها تدعم TypeScript خارج الصندوق.
تشغيل كود المستخدم هو سيف ذو حدين. يمكن أن يوفر الكثير من الوظائف والتخصيص لنظامك الأساسي، ولكنه يعرضك أيضًا لمخاطر أمنية كبيرة. من الضروري فهم المخاطر واتخاذ التدابير المناسبة للتخفيف منها وتذكر أنه كلما كانت البيئة معزولة، أصبحت أكثر أمانًا.
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3