
صندوق رمل (Sandbox) معزول للغة JavaScript في بيئة Node.js، يعمل على تشغيل الكود غير الموثوق مع تقييد الوصول إلى الوحدات المدمجة وموارد المضيف عبر الاعتراض القائم على Proxy.
vm2 عبارة عن صندوق رمل (sandbox) يمكنه تشغيل كود غير موثوق به مع وحدات Node.js المدمجة المدرجة في القائمة البيضاء.
قبل استخدام vm2، يجب أن تفهم كيف يعمل وقيوده.
vm2 يحاول عزل كود JavaScript غير الموثوق به ضمن نفس عملية Node.js التي يعمل بها تطبيقك. يقوم بذلك من خلال شبكة معقدة من الوكلاء (Proxies) التي تعترض وتتوسط كل تفاعل بين الصندوق الرملي والبيئة المضيفة.
JavaScript لغة ديناميكية بشكل استثنائي. يمكن الوصول إلى الكائنات من خلال سلاسل النماذج الأولية (prototype chains)، ويمكن الوصول إلى المُنشئات عبر كائنات الأخطاء، وتوفر الرموز (symbols) خطافات للبروتوكولات، وينشئ التنفيذ غير المتزامن نوافذ توقيت. العدد الهائل من الطرق للانتقال من كائن إلى آخر في JavaScript يجعل بناء صندوق رمل محكم الإغلاق داخل العملية أمرًا بالغ الصعوبة.
نحن صادقون بشأن هذه الحقيقة: على الرغم من أفضل جهودنا، يكتشف الباحثون ومتخصصو الأمن باستمرار طرقًا جديدة للهروب من صندوق الرمل vm2. نقوم بنشاط بتصحيح هذه الثغرات عند الإبلاغ عنها، لكن طبيعة لعبة القط والفأر في العزل داخل العملية تعني أن:
إذا كنت بحاجة إلى ضمانات عزل أقوى، ففكر في هذه البدائل التي توفر عزلًا حقيقيًا على مستوى العملية أو العتاد:
يمكن أن يكون vm2 مناسبًا عندما:
إذا كنت تشغّل كودًا من مصادر غير موثوقة تمامًا (مثل إرسالات المستخدمين العشوائية)، فنوصي بشدة باستخدام حل بضمانات عزل أقوى.
للاطلاع المتعمق على داخل vm2، راجع ملف CONTRIBUTING.md.
جرّب بنفسك:```js import { runInNewContext } from "node:vm";
runInNewContext('this.constructor.constructor("return process")().exit()'); console.log('Never gets executed.');
يبدو أن نص chunk 3 لم يُرفَق مع الطلب. يرجى إعادة إرسال المحتوى المطلوب ترجمته.```js
import { VM } from 'vm2';
new VM().run('this.constructor.constructor("return process")().exit()');
// Throws ReferenceError: process is not defined
npm install vm2
## أمثلة سريعة```js
import { VM } from 'vm2';
const vm = new VM();
vm.run(`process.exit()`); // TypeError: process.exit is not a function
أو فحص النطاقات الفرعية وتنفيذ بعض الميزات الأخرى.
متاح: مجاني ومدفوع```js
import { NodeVM } from 'vm2';
const vm = new NodeVM({ require: { external: true, root: './', }, });
vm.run(
var request = require('request'); request('http://www.google.com', function (error, response, body) { console.error(error); if (!error && response.statusCode == 200) { console.log(body); // Show the HTML for the Google homepage. } });,
'vm.js',
);
## Documentation
- [VM](#vm)
- [NodeVM](#nodevm)
- [VMScript](#vmscript)
- [معالجة الأخطاء](#error-handling)
- [تصحيح أخطاء الكود المعزول](#debugging-a-sandboxed-code)
- [الكائنات للقراءة فقط](#read-only-objects-experimental)
- [الكائنات المحمية](#protected-objects-experimental)
- [العلاقات بين العزلات](#cross-sandbox-relationships)
- [سطر الأوامر](#cli)
- [تغييرات الإصدار 2.x إلى 3.x](https://github.com/patriksimek/vm2/wiki/2.x-to-3.x-changes)
- [وثائق الإصدارين 1.x و2.x](https://github.com/patriksimek/vm2/wiki/1.x-and-2.x-docs)
- [المساهمة](https://github.com/patriksimek/vm2/wiki/Contributing)
## VM
VM عبارة عن صندوق رمل بسيط لتشغيل الكود غير الموثوق بشكل متزامن دون ميزة `require`. تتوفر فقط كائنات JavaScript المدمجة وكائن `Buffer` الخاص بـ Node. دوال الجدولة (`setInterval`، `setTimeout` و`setImmediate`) غير متوفرة افتراضيًا.
**الخيارات:**
- `timeout` - مهلة تنفيذ السكريبت بالمللي ثانية. **تحذير**: قد ترغب في استخدام هذا الخيار مع `allowAsync=false`. علاوة على ذلك، فإن التعامل مع الكائنات المُعادة من الصندوق الرملي يمكن أن يشغل كودًا عشوائيًا ويلتفي على المهلة. يجب على المرء اختبار ما إذا كان الكائن المُعاد بدائيًا باستخدام `typeof` والتخلص منه تمامًا (حيث أن تسجيل الدخول أو إنشاء رسائل خطأ باستخدام مثل هذا الكائن قد يشغل أيضًا كودًا عشوائيًا مرة أخرى) في الحالة الأخرى.
- `sandbox` - الكائن العام لـ VM.
- `compiler` - `javascript` (الافتراضي)، `typescript`، `coffeescript` أو دالة مترجم مخصصة. تتوقع المكتبة أن يكون لديك المترجم مثبتًا مسبقًا إذا تم تعيين القيمة على `typescript` أو `coffeescript`.
- `eval` - إذا تم تعيينه على `false`، فإن أي استدعاءات لـ `eval` أو مُنشئات الدوال (`Function`، `GeneratorFunction`، إلخ) ستُحدث خطأ `EvalError` (الافتراضي: `true`).
- `wasm` - إذا تم تعيينه على `false`، فإن أي محاولة لتجميع وحدة WebAssembly ستُحدث خطأ `WebAssembly.CompileError` (الافتراضي: `true`). ملاحظة: تتم إزالة `WebAssembly.JSTag` داخل الصندوق الرملي لأسباب أمنية، لذا لا يمكن لكود wasm التقاط استثناءات JavaScript.
- `allowAsync` - إذا تم تعيينه على `false`، فإن أي محاولة لتشغيل كود باستخدام `async` ستُحدث خطأ `VMError` (الافتراضي: `true`).
- `bufferAllocLimit` - الحد الأقصى للحجم بالبايت لطلب واحد من `Buffer.alloc` / `Buffer.allocUnsafe` / `Buffer.allocUnsafeSlow` / `Buffer(N)` / `new Buffer(N)` من داخل الصندوق الرملي. الطلبات التي تتجاوز هذا الحد تُحدث خطأ `RangeError` بشكل متزامن دون تنفيذ التخصيص على المضيف. الافتراضي: `Infinity` (بدون حد، متوافق تمامًا مع الإصدارات السابقة). يجب على المشغّلين الذين يشغّلون كودًا غير موثوق في بيئات محدودة الذاكرة (Docker / Kubernetes / Lambda / serverless) اختيار حد محدود (مثل `32 * 1024 * 1024`) كجزء من الدفاع متعدد الطبقات ضد DoS، بنفس الطريقة التي يختارون بها `timeout`. انظر [توصيات التقوية](#hardening-recommendations) أدناه.
**هام**: المهلة فعّالة فقط على الكود المتزامن الذي تشغّله عبر `run`. المهلة **لا** تعمل على أي طريقة مُعادة من VM. هناك بعض الحالات التي لا تعمل فيها المهلة - انظر [#244](https://github.com/patriksimek/vm2/pull/244).```js
import { VM } from 'vm2';
const vm = new VM({
timeout: 1000,
allowAsync: false,
sandbox: {},
});
vm.run('process.exit()'); // throws ReferenceError: process is not defined
يمكنك أيضًا استرداد القيم من VM.```js let number = vm.run('1337'); // returns 1337
**نصيحة**: راجع الاختبارات لمزيد من أمثلة الاستخدام.
## NodeVM
على عكس `VM`، يتيح لك `NodeVM` استيراد الوحدات بالطريقة نفسها التي تتبعها في سياق Node العادي.
**الخيارات:**
- `console` - `inherit` لتمكين وحدة التحكم، و`redirect` لإعادة التوجيه إلى الأحداث، و`off` لتعطيل وحدة التحكم (الافتراضي: `inherit`).
- `sandbox` - الكائن العام للـ VM.
- `compiler` - `javascript` (الافتراضي)، أو `typescript`، أو `coffeescript`، أو دالة مترجم مخصصة (تستقبل الكود ومسار ملفه). تتوقع المكتبة أن يكون المترجم مثبتًا مسبقًا إذا كانت القيمة مضبوطة على `typescript` أو `coffeescript`.
- `eval` - إذا تم ضبطه على `false`، فإن أي استدعاءات لـ `eval` أو مُنشئات الدوال (`Function`، `GeneratorFunction`، إلخ) ستُطلق خطأ `EvalError` (الافتراضي: `true`).
- `wasm` - إذا تم ضبطه على `false`، فإن أي محاولة لتجميع وحدة WebAssembly ستُطلق `WebAssembly.CompileError` (الافتراضي: `true`). ملاحظة: تتم إزالة `WebAssembly.JSTag` داخل العزلة (sandbox) لأسباب أمنية، لذا لا يمكن لرمز wasm التقاط استثناءات JavaScript.
- `bufferAllocLimit` - نفس دلالات الخيار في `VM` — الحد الأقصى للحجم بالبايت لطلب واحد من عائلة `Buffer.alloc` من داخل العزلة. الافتراضي: `Infinity`. راجع [توصيات التحصين](#hardening-recommendations).
- `sourceExtensions` - مصفوفة من امتدادات الملفات التي سيتم التعامل معها ككود مصدري (الافتراضي: `['js']`).
- `require` - `true` أو كائن أو `Resolver` لتفعيل دالة `require` (الافتراضي: `false`).
- `require.external` - يمكن أن تكون القيم `true` أو مصفوفة من الوحدات الخارجية المسموح بها أو كائنًا (الافتراضي: `false`). يُسمح باستيراد جميع المسارات المطابقة للنمط `/node_modules/${any_allowed_external_module}/(?!/node_modules/)`.
- `require.external.modules` - مصفوفة من الوحدات الخارجية المسموح بها. كما تدعم أحرف البدل (wildcards)، فعلى سبيل المثال تحديد `['@scope/*-ver-??]` سيسمح باستخدام جميع الوحدات التي يكون اسمها بالشكل `@scope/something-ver-aa`، أو `@scope/other-ver-11`، إلخ. لا يطابق حرف البدل `*` فواصل المسارات.
- `require.external.transitive` - قيمة منطقية تشير إلى ما إذا كانت التبعيات غير المباشرة (transitive) للوحدات الخارجية مسموحًا بها (الافتراضي: `false`). **تحذير**: عندما يتم استيراد وحدة بشكل غير مباشر، يمكن لأي وحدة بعد ذلك استيرادها بشكل طبيعي، حتى لو لم يكن ذلك ممكنًا قبل تحميلها.
- `require.builtin` - مصفوفة من الوحدات المدمجة المسموح بها، تقبل ["\*"] للسماح بجميعها (الافتراضي: لا شيء). **تحذير**: قد يكون "\*" خطيرًا حيث يمكن إضافة وحدات مدمجة جديدة.
- `require.root` - المسار (أو المسارات) المقيّدة التي يمكن من خلالها استيراد الوحدات المحلية (الافتراضي: كل المسارات).
- `require.mock` - مجموعة من الوحدات الوهمية (mock modules) (سواء كانت خارجية أو مدمجة).
- `require.context` - `host` (الافتراضي) لاستيراد الوحدات في المضيف (host) وإعادة توجيهها (proxy) إلى العزلة. `sandbox` لتحميل وتجميع واستيراد الوحدات داخل العزلة. `callback(moduleFilename, ext)` لاختيار سياق لكل وحدة بشكل ديناميكي. إذا لم يتم تحديد أي شيء، يكون الافتراضي هو `sandbox`. باستثناء `events`، يتم دائمًا استيراد الوحدات المدمجة في المضيف وإعادة توجيهها إلى العزلة.
- `require.import` - مصفوفة من الوحدات التي سيتم تحميلها داخل NodeVM عند بدء التشغيل.
- `require.resolve` - دالة بحث إضافية تُستخدم في حال عدم العثور على وحدة في إحدى مسارات البحث التقليدية في Node.
- `require.customRequire` - يُستخدم بدلاً من دالة `require` لتحميل الوحدات من المضيف.
- `require.strict` - عيّنه إلى `false` لعدم فرض الوضع الصارم (strict mode) على الوحدات المحمّلة عبر require (الافتراضي: `true`).
- `require.fs` - تنفيذ مخصص لنظام الملفات.
- `nesting` - **تحذير**: السماح بهذا يشكّل خطرًا أمنيًا لأن البرامج النصية يمكنها إنشاء NodeVM قادر على استيراد أي وحدة من المضيف. عيّنه إلى `true` لتفعيل تداخل VMs (الافتراضي: `false`).
- `wrapper` - `commonjs` (الافتراضي) للفّ السكربت داخل غلاف CommonJS، أو `none` لاسترجاع القيمة التي يُرجعها السكربت.
- `argv` - مصفوفة يتم تمريرها إلى `process.argv`.
- `env` - كائن يتم تمريره إلى `process.env`.
- `strict` - `true` لتحميل الوحدات في الوضع الصارم (الافتراضي: `false`).
**مهم**: لا تكون المهلة (Timeout) فعّالة مع NodeVM، لذا فهو ليس محصّنًا ضد `while (true) {}` أو ما شابه ذلك من الأكواد الخبيثة.
**تذكّر**: كلما سمحت بعدد أكبر من الوحدات، أصبحت عزلة (sandbox) الخاصة بك أكثر هشاشة.```js
import { NodeVM } from 'vm2';
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: {
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!',
},
},
},
});
// Sync
let functionInSandbox = vm.run('module.exports = function(who) { console.log("hello "+ who); }');
functionInSandbox('world');
// Async
let functionWithCallbackInSandbox = vm.run('module.exports = function(who, callback) { callback("hello "+ who); }');
functionWithCallbackInSandbox('world', greeting => {
console.log(greeting);
});
When wrapper is set to none, NodeVM behaves more like VM for synchronous code.
عندما يتم ضبط wrapper على none، يتصرف NodeVM بشكل أقرب إلى VM بالنسبة للكود المتزامن.```js
assert.ok(vm.run('return true') === true);
**نصيحة**: راجع الاختبارات لمزيد من أمثلة الاستخدام.
### تحميل الوحدات عبر المسار النسبي
لتحميل الوحدات عبر مسار نسبي، يجب تمرير المسار الكامل للسكربت الذي تقوم بتشغيله كوسيطة ثانية لأسلوب `run` في vm إذا كان السكربت نصًا. يتم عرض اسم الملف بعد ذلك في أي تتبعات مكدس يولّدها السكربت.```js
vm.run('require("foobar")', '/data/myvmscript.js');
إذا كان السكربت الذي تقوم بتشغيله هو VMScript، فالمسار مُعطى في مُنشئ VMScript.```js const script = new VMScript('require("foobar")', { filename: '/data/myvmscript.js' }); vm.run(script);
### Resolver
يمكن إنشاء Resolver عبر `makeResolverFromLegacyOptions` واستخدامه لعدة مثيلات `NodeVM`، مما يتيح مشاركة كود الوحدات المترجمة وربما تسريع أوقات التحميل. يمكن إعادة كتابة المثال الأول من `NodeVM` باستخدام `makeResolverFromLegacyOptions` على النحو التالي.```js
const resolver = makeResolverFromLegacyOptions({
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!',
},
},
});
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: resolver,
});
يمكنك زيادة الأداء باستخدام البرامج النصية المجمّعة مسبقًا. يمكن تشغيل VMScript المجمّع مسبقًا عدة مرات. من المهم ملاحظة أن الكود غير مرتبط بأي VM (سياق)؛ بل يتم ربطه قبل كل تشغيل، فقط لهذا التشغيل.```js import { VM, VMScript } from 'vm2';
const vm = new VM(); const script = new VMScript('Math.random()'); console.log(vm.run(script)); console.log(vm.run(script));
إنه يعمل لكل من `VM` و`NodeVM`.```js
import { NodeVM, VMScript } from 'vm2';
const vm = new NodeVM();
const script = new VMScript('module.exports = Math.random()');
console.log(vm.run(script));
console.log(vm.run(script));
يتم تجميع الكود تلقائيًا عند أول تشغيل له. يمكن للمستخدم تجميع الكود في أي وقت باستخدام script.compile(). بمجرد تجميع الكود، لا يكون لهذه الطريقة أي تأثير.
يمكن التعامل مع الأخطاء في تجميع الكود وتنفيذ الكود المتزامن باستخدام try-catch. يمكن التعامل مع الأخطاء في تنفيذ الكود غير المتزامن من خلال إرفاق معالج الأحداث uncaughtException بكائن process الخاص بـ Node.```js
try {
var script = new VMScript('Math.random()').compile();
} catch (err) {
console.error('Failed to compile script.', err);
}
try { vm.run(script); } catch (err) { console.error('Failed to execute script.', err); }
process.on('uncaughtException', err => { console.error('Asynchronous error caught.', err); });
## تصحيح أخطاء الكود المعزول
يمكنك تصحيح أو فحص الكود الذي يعمل داخل العزلة (sandbox) كما لو كان يعمل في عملية عادية.
- يمكنك استخدام نقاط التوقف (breakpoints) (وهو ما يتطلب تحديد اسم ملف السكربت)
- يمكنك استخدام الكلمة المفتاحية `debugger`.
- يمكنك استخدام خطوة-إلى-الداخل (step-in) للتنقل داخل الكود الذي يعمل في العزلة.
### مثال
/tmp/main.js:```js
import { VM, VMScript } from 'vm2';
import { readFileSync } from 'node:fs';
const file = `${__dirname}/sandbox.js`;
// By providing a file name as second argument you enable breakpoints
const script = new VMScript(readFileSync(file), file);
new VM().run(script);
/tmp/sandbox.js```js const foo = 'ahoj';
// The debugger keyword works just fine everywhere. // Even without specifying a file name to the VMScript object. debugger;
## كائنات للقراءة فقط (تجريبية)
لمنع البرامج النصية المعزولة من إضافة أو تغيير أو حذف الخصائص من الكائنات الوكيلة، يمكنك استخدام طرق `freeze` لجعل الكائن للقراءة فقط. هذا فعّال فقط داخل VM. الكائنات المجمَّدة تتأثر بعمق. لا يمكن تجميد الأنواع البدائية.
**مثال بدون استخدام `freeze`:**```js
const util = {
add: (a, b) => a + b,
};
const vm = new VM({
sandbox: { util },
});
vm.run('util.add = (a, b) => a - b');
console.log(util.add(1, 1)); // returns 0
مثال باستخدام freeze:```js
const vm = new VM(); // Objects specified in the sandbox cannot be frozen.
vm.freeze(util, 'util'); // Second argument adds object to global.
vm.run('util.add = (a, b) => a - b'); // Fails silently when not in strict mode. console.log(util.add(1, 1)); // returns 2
**مهم:** لا يمكن تجميد الكائنات التي سبق تمريرها عبر وكيل إلى VM.
## الكائنات المحمية (تجريبي)
على عكس `freeze`، تسمح هذه الطريقة للسكربتات المعزولة بإضافة الخصائص أو تغييرها أو حذفها على الكائنات، مع استثناء واحد - لا يمكن إرفاق دوال. لذلك لا تستطيع السكربتات المعزولة تعديل دوال مثل `toJSON` أو `toString` أو `inspect`.
**مهم:** لا يمكن حماية الكائنات التي سبق تمريرها عبر وكيل إلى VM.
## العلاقات عبر المعزولات```js
const assert = require('assert');
const { VM } = require('vm2');
const sandbox = {
object: new Object(),
func: new Function(),
buffer: new Buffer([0x01, 0x05]),
};
const vm = new VM({ sandbox });
assert.ok(vm.run(`object`) === sandbox.object);
assert.ok(vm.run(`object instanceof Object`));
assert.ok(vm.run(`object`) instanceof Object);
assert.ok(vm.run(`object.__proto__ === Object.prototype`));
assert.ok(vm.run(`object`).__proto__ === Object.prototype);
assert.ok(vm.run(`func`) === sandbox.func);
assert.ok(vm.run(`func instanceof Function`));
assert.ok(vm.run(`func`) instanceof Function);
assert.ok(vm.run(`func.__proto__ === Function.prototype`));
assert.ok(vm.run(`func`).__proto__ === Function.prototype);
assert.ok(vm.run(`new func() instanceof func`));
assert.ok(vm.run(`new func()`) instanceof sandbox.func);
assert.ok(vm.run(`new func().__proto__ === func.prototype`));
assert.ok(vm.run(`new func()`).__proto__ === sandbox.func.prototype);
assert.ok(vm.run(`buffer`) === sandbox.buffer);
assert.ok(vm.run(`buffer instanceof Buffer`));
assert.ok(vm.run(`buffer`) instanceof Buffer);
assert.ok(vm.run(`buffer.__proto__ === Buffer.prototype`));
assert.ok(vm.run(`buffer`).__proto__ === Buffer.prototype);
assert.ok(vm.run(`buffer.slice(0, 1) instanceof Buffer`));
assert.ok(vm.run(`buffer.slice(0, 1)`) instanceof Buffer);
قبل أن تتمكن من استخدام vm2 في سطر الأوامر، ثبّته عالميًا باستخدام npm install vm2 -g.```sh
vm2 ./script.js
## توصيات التقوية
يمنع vm2 عمليات الهروب من وضع الحماية (حصول الكود غير الموثوق على وصول إلى نطاق المضيف). لكنه **لا** يمنع، بمفرده، كل أشكال استنزاف الموارد أو هجمات حجب الخدمة. يجب على المُضمّنين الذين يشغّلون كودًا غير موثوق إضافة الدفاعات الطبقية التالية حول وضع الحماية.
### 1. تقييد تخصيص الذاكرة باستخدام `bufferAllocLimit`
نداء واحد `Buffer.alloc(N)` مع `N` يتحكم فيه المهاجم يُنفَّذ كتخصيص C++ متزامن واحد على المضيف لا يمكن لـ `timeout` الخاص بـ V8 مقاطعته. في البيئات المحدودة الذاكرة، يمكن أن تؤدي حمولة صغيرة في وضع الحماية بحوالي 100 بايت إلى قفزة في RSS للمضيف تتجاوز 100 ميغابايت وتعطل عملية المضيف عبر OOM. اضبط `bufferAllocLimit` (مثل `32 * 1024 * 1024`) لتقييد التخصيصات الفردية:```js
const vm = new VM({
timeout: 1000,
bufferAllocLimit: 32 * 1024 * 1024,
allowAsync: false,
});
السقف ينطبق أيضًا على المسارات المهملة Buffer(N) و new Buffer(N). لاحظ أن الاستنزاف التجميعي (العديد من التخصيصات الصغيرة، Buffer.concat، Uint8Array، String.repeat، Array(n).fill()، إلخ.) لا يغطيه هذا السقف — فاجمع بين السقف وحد ذاكرة على جانب المضيف (--max-old-space-size، حد الحاوية، cgroup) لتغطية كاملة.
unhandledRejection على جانب المضيفتوجد فئة من هجمات رفض الخدمة (DoS) بإحباط عملية المضيف حيث ينشئ كود الصندوق الرمل async function أو async function* أو await using، ويرمي جسمه قيمة تؤدي إلى خطأ في نطاق المضيف أثناء تنسيق المكدس (مثل e.name = Symbol(); e.stack). ينشئ V8 وعد الرفض عبر Promise الجوهري للنطاق، مما يتجاوز غلاف فئة vm2 الفرعية Promise، لذا يهرب الرفض إلى المضيف كـ unhandledRejection. في Node 15+ السلوك الافتراضي هو إنهاء العملية.
إغلاق هذه الثغرة يتطلب تغيير سلوك المضيف الملحوظ، لذلك لا توفر vm2 إصلاحًا افتراضيًا. يجب على المُضمِّنين تثبيت معالج على مستوى العملية يبتلع (أو يسجل) حالات الرفض الناشئة عن الصندوق الرمل:```js // Recommended: filter rejections that originated inside vm2 and swallow them, // while letting your own host-side rejections propagate. process.on('unhandledRejection', (reason, promise) => { // Heuristic: rejections from the sandbox frequently surface as values // without proper Error semantics, or with stacks pointing at vm.js. // Adjust the predicate to match your application. if (looksLikeSandboxOrigin(reason)) { return; // swallow — don't terminate the process } // Otherwise: handle (or rethrow) as normal for your host code. yourLogger.error('unhandled rejection', reason); });
إذا لم يكن لدى تطبيقك أي مصدر آخر للرفض غير المُعالَج، فإن الابتلاع الشامل مع التسجيل مقبول:```js
process.on('unhandledRejection', reason => {
yourLogger.warn('swallowed sandbox rejection', reason);
});
قد يُطرح إصلاح محدود النطاق خلف علامة اختيارية swallowSandboxUnhandledRejections في إصدار ثانوي مستقبلي؛ وحتى ذلك الحين، يُعد معالج جانب المضيف التخفيف الموصى به.
حتى مع ضبط bufferAllocLimit، شغّل عملية المضيف مع --max-old-space-size (أو حد ذاكرة حاوية مكافئ) بحجم يناسب عبء العمل. يحمي الحد الأقصى من أولية التخصيص الفردي؛ ويحمي حد مستوى نظام التشغيل من الاستنزاف الكلي ومن أي أولية تخصيص مستقبلية لم تقم vm2 بتقييدها بعد.
require.builtin: ['*'] كإعداد غير معزولتتوسع علامة البدل '*' إلى معظم الوحدات المدمجة في Node، بما في ذلك child_process وfs وdgram وnet وhttp وdns. هذه أوليات كاملة بقدرات المضيف — إذ يمكن الوصول إلى require('child_process').execSync('id') من داخل العزل تحت '*'. دلالات '*' في vm2 مقصودة (بعض المُضمّنين يشغّلون كودًا موثوقًا لكنه معزول)، لكن لا ينبغي استخدامها كإعداد افتراضي للكود غير الموثوق. فضّل قائمة سماح صريحة بأصغر مجموعة وحدات يحتاجها العزل فعليًا.
nesting: true هو مخرج طوارئيتيح nesting: true لكود العزل تنفيذ require('vm2') وإنشاء NodeVMs متداخلة. إعداد require للـ VM المتداخل يُختار بواسطة كود العزل الذي ينشئه، ولا يُقيَّد بواسطة الـ VM الخارجي. بشكل ملموس:```js
const vm = new NodeVM({ nesting: true, require: { builtin: [] } });
vm.run( const { NodeVM: NVM } = require('vm2'); // Inner VM's config is whatever the sandbox writes here: const inner = new NVM({ require: { builtin: ['child_process'] } }); inner.run('require("child_process").execSync("id")'); // RCE);
إذا قمت بتعيين `nesting: true`، فإنك تمنح الصندوق الرملي فعليًا نفس مستوى الثقة الذي لديك. **لا تقم بتمكين `nesting: true` للتعليمات البرمجية غير الموثوقة.** استخدمه فقط عندما تثق في الكود المعزول نفسه، ولكنك تريد دلالات تنفيذ على غرار VM (نطاق عام جديد، مهلات زمنية مضبوطة) لأسباب غير متعلقة بالأمان.
`nesting: true` **يتطلب كائن إعدادات `require` صريحًا** (مثل `require: { builtin: [] }` أو `require: {}`). أي شكل آخر — `require: false` أو `require: undefined` أو `require: null` أو حذف `require` تمامًا — يرمي خطأ `VMError` عند الإنشاء (GHSA-m4wx-m65x-ghrr، والذي يحل محل GHSA-8hg8-63c5-gwmx). جميع هذه الأشكال تنتج محللًا حصريًا لـ NESTING_OVERRIDE فقط: حيث يمكن للصندوق الرملي استدعاء `require('vm2')` فقط دون أي شيء آخر، وهو بدائية هروب (escape primitive) خالصة لا استخدام مشروع لها. لمنع جميع عمليات الاستدعاء `require`، قم بإزالة `nesting: true`. وللسماح ببيئات VMs متداخلة، قم بتوفير إعدادات `require` صريحة بحيث تكون المقايضة مرئية في موقع الاستدعاء.
## المشكلات المعروفة
- ليس من الممكن تعريف فئة تمتد لفئة مُوكَّلة. يشمل ذلك استخدام فئة مُوكَّلة في `Object.create`.
- لا يعمل استدعاء `eval` المباشر.
- تسجيل مصفوفات الصندوق الرملي سيؤدي إلى تكرار الجزء المصفوفي في الخصائص.
- يمكن أن تؤدي تحويلات الكود المصدري إلى اختلاف سلسلة المصدر الخاصة بالدالة.
- توجد طرق لتعطيل عملية node من داخل الصندوق الرملي. راجع [توصيات التقوية](#hardening-recommendations).
[npm-image]: https://img.shields.io/npm/v/vm2.svg
[npm-url]: https://www.npmjs.com/package/vm2
[license-image]: https://img.shields.io/npm/l/vm2.svg
[license-url]: https://raw.githubusercontent.com/patriksimek/vm2/resurrection/LICENSE.md
[downloads-image]: https://img.shields.io/npm/dm/vm2.svg
[downloads-url]: https://www.npmjs.com/package/vm2
[snyk-image]: https://snyk.io/test/github/patriksimek/vm2/badge.svg
[snyk-url]: https://snyk.io/test/github/patriksimek/vm2
| الحل | الأسلوب | الأداء | المفاضلات |
|---|
| isolated-vm | عزلات V8 منفصلة (heap مختلف لـ V8) | سريع | في وضع الصيانة؛ يتطلب تحديثات يدوية لـ V8 |
| عملية منفصلة / Worker | عمليات child_process أو خيوط Worker بصلاحيات محدودة | متوسط | حمل زائد أعلى في IPC؛ يجب إجراء تسلسل (serialization) للبيانات |
| حاويات / أجهزة افتراضية | Docker، gVisor، Firecracker | بطيء | حمل زائد عند الإقلاع؛ كثيف الاستخدام للموارد |
| خدمات مُدارة | تنفيذ كود قائم على السحابة (مثل AWS Lambda، Cloudflare Workers) | متغير | زمن استجابة الشبكة؛ اعتماد على طرف خارجي |