
Proxy 기반 가로채기를 통해 내장 모듈 및 호스트 리소스에 대한 액세스를 제한하면서 신뢰할 수 없는 코드를 실행하는 Node.js용 격리 JavaScript 샌드박스
vm2는 허용 목록에 등록된 Node의 내장 모듈로 신뢰할 수 없는 코드를 실행할 수 있는 샌드박스입니다.
npm install vm2
## 빠른 예제```js
import { VM } from 'vm2';
const vm = new VM();
vm.run(`process.exit()`); // TypeError: process.exit is not a function
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',
);
## 중요 보안 고지
**vm2를 사용하기 전에, vm2가 어떻게 작동하며 어떤 한계가 있는지 이해해야 합니다.**
vm2는 신뢰할 수 없는 JavaScript 코드를 애플리케이션과 **동일한 Node.js 프로세스 내에서** 샌드박싱하려고 시도합니다. 이는 샌드박스와 호스트 환경 간의 모든 상호작용을 가로채고 중재하는 복잡한 [Proxy](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 네트워크를 통해 이루어집니다.
### 근본적인 과제
JavaScript는 매우 역동적인 언어입니다. 객체는 프로토타입 체인을 통해 접근할 수 있고, 생성자는 오류 객체를 통해 도달할 수 있으며, 심볼은 프로토콜 훅을 제공하고, 비동기 실행은 타이밍 창을 만듭니다. JavaScript에서 한 객체에서 다른 객체로 이동할 수 있는 수많은 방법은 완벽한 프로세스 내 샌드박스를 구축하는 것을 극도로 어렵게 만듭니다.
**우리는 이러한 현실에 대해 솔직합니다:** 최선을 다했음에도 불구하고, 연구자와 보안 전문가들은 vm2 샌드박스를 탈출하는 새로운 방법을 지속적으로 발견하고 있습니다. 우리는 이러한 취약점이 보고될 때마다 적극적으로 패치하지만, 프로세스 내 샌드박싱의 숨바꼭질 같은 특성은 다음을 의미합니다:
1. **새로운 우회 방법이 향후 발견될 가능성이 높습니다.** 알려진 취약점은 [보안 권고](https://github.com/patriksimek/vm2/security/advisories)에서 확인하세요.
2. **최신 보안 수정 사항을 적용하려면 vm2를 업데이트 상태로 유지해야 합니다.** 보안 권고를 구독하고 신속하게 업데이트하세요.
3. **vm2가 유일한 방어선이 되어서는 안 됩니다.** 신뢰할 수 없는 코드를 실행할 때는 심층 방어가 필수적입니다.
### 더 강력한 대안
더 강력한 격리 보장이 필요하다면, **진정한 프로세스 또는 하드웨어 수준 격리**를 제공하는 다음 대안을 고려하세요:
| 솔루션 | 접근 방식 | 성능 | 트레이드오프 |
|----------|----------|-------------|------------|
| **[isolated-vm](https://github.com/laverdet/isolated-vm)** | 별도의 V8 isolate(별도의 V8 힙) | 빠름 | 유지보수 모드; 수동 V8 업데이트 필요 |
| **별도 프로세스 / Worker** | 제한된 권한의 `child_process` 또는 Worker 스레드 | 중간 | 높은 IPC 오버헤드; 데이터 직렬화 필요 |
| **컨테이너 / VM** | Docker, gVisor, Firecracker | 느림 | 시작 오버헤드; 리소스 집약적 |
| **관리형 서비스** | 클라우드 기반 코드 실행(예: AWS Lambda, Cloudflare Workers) | 가변적 | 네트워크 지연; 외부 의존성 |
### vm2가 여전히 적합할 수 있는 경우
vm2는 다음과 같은 경우에 적합할 수 있습니다:
- 호스트 객체와의 긴밀한 통합과 빠른 동기 통신이 필요한 경우
- 신뢰할 수 없는 코드가 비교적 신뢰할 수 있는 소스에서 온 경우(예: 내부 도구, 검증된 작성자가 있는 플러그인 시스템)
- vm2를 다른 보안 계층(네트워크 격리, 파일시스템 제한, 리소스 제한)과 결합하는 경우
- 위험을 수용하고 보안 업데이트를 적극적으로 모니터링하는 경우
**완전히 신뢰할 수 없는 소스(예: 임의의 사용자 제출물)의 코드를 실행하는 경우, 더 강력한 격리 보장을 제공하는 솔루션을 사용할 것을 강력히 권장합니다.**
## 런타임
| 런타임 | 상태 |
|---------|--------|
| Node.js | 지원됨. 샌드박스는 보안 경계입니다. |
| Bun | **실험적.** 부분적 기능 호환 — **보안 경계가 아님.** |
Bun에는 두 가지 별개의 제한 사항이 적용되며, 어느 것도 다른 것을 의미하지 않습니다.
**보안 경계가 아닙니다.** vm2의 위협 모델, [`docs/ATTACKS.md`](https://github.com/patriksimek/vm2/blob/main/docs/ATTACKS.md)의 공격 카탈로그, 그리고 `test/ghsa/`의 모든 회귀 테스트는 V8 내부 구조에서 파생되었습니다. Bun이 사용하는 JavaScriptCore에는 자체적인 동등한 구조가 있지만, vm2의 브리지에 대해 감사된 바는 없습니다. Bun에서 테스트 스위트가 통과하는 것은 호환성을 입증할 뿐, 샌드박스가 그곳에서 유지된다는 것을 입증하지 않습니다. **Bun에서 vm2를 사용하여 신뢰할 수 없는 코드를 격리하지 마세요.**
**호환성은 부분적이며, 동등성이 아닙니다.** Bun에서의 성공적인 실행은 실제로 그곳에서 실행되는 테스트만을 다룹니다. `test/bun-skips.js`는 무엇이 제외되었고 그 이유가 무엇인지 나열하며, 알려진 동작 차이는 다음과 같습니다:
- `Buffer.from(arrayLike)`는 길이가 0인 버퍼를 반환합니다
- `VMScript`의 `filename` / `lineOffset` / `columnOffset` 메타데이터는 JSC의 CallSite 객체에 메서드가 없기 때문에 관찰할 수 없습니다
- 비구성 가능한 접근자가 있는 동결된 호스트 객체에 대한 `Object.freeze`는 V8에서는 발생하지 않는 프록시 불변식 `TypeError`를 발생시킵니다
- 샌드박스 경계를 넘는 일부 `Buffer` 작업은 극도로 느립니다 — 64MB `allocUnsafe`는 Node의 1.7초에 비해 400초 이상 걸리며, 멈춤으로 읽힐 만큼 느립니다
Bun 지원은 신뢰할 수 있는 코드를 위한 최선의 호환성으로 취급하고, 특정 동작에 의존하기 전에 건너뛰기 목록을 확인하세요.
## 기능
- 신뢰할 수 없는 코드를 단일 프로세스에서 안전하게 실행하며 코드와 나란히 실행
- 샌드박스의 콘솔 출력에 대한 완전한 제어
- 샌드박스는 프로세스의 메서드에 대한 제한된 접근 권한을 가짐
- 샌드박스에서 모듈(내장 및 외부)을 require할 수 있음
- 특정(또는 모든) 내장 모듈에 대한 접근을 제한할 수 있음
- 샌드박스 간에 메서드를 안전하게 호출하고 데이터와 콜백을 교환할 수 있음
- 알려진 탈출 방법에 대한 패치로 적극적으로 유지관리됨([보안 고지](#important-security-disclaimer) 참조)
- 트랜스파일러 지원
## 작동 방식
- 내부 VM 모듈을 사용하여 보안 컨텍스트를 생성합니다.
- [Proxy](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)를 사용하여 샌드박스 탈출을 방지합니다.
- 내장 require를 재정의하여 모듈 접근을 제어합니다.
vm2의 내부 구조에 대한 심층 분석은 [docs/ATTACKS.md](https://github.com/patriksimek/vm2/blob/main/docs/ATTACKS.md)를 참조하세요.
## Node의 vm과 vm2의 차이점은 무엇인가요?
직접 시도해 보세요:```js
import { runInNewContext } from "node:vm";
runInNewContext('this.constructor.constructor("return process")().exit()');
console.log('Never gets executed.');
I'm ready to translate the Kitploit tool content from English to Korean. Please provide chunk 9 of 55.```js import { VM } from 'vm2';
new VM().run('this.constructor.constructor("return process")().exit()'); // Throws ReferenceError: process is not defined
## 문서
- [VM](#vm)
- [NodeVM](#nodevm)
- [VMScript](#vmscript)
- [오류 처리](#error-handling)
- [샌드박스 코드 디버깅](#debugging-a-sandboxed-code)
- [읽기 전용 객체](#read-only-objects-experimental)
- [보호된 객체](#protected-objects-experimental)
- [샌드박스 간 관계](#cross-sandbox-relationships)
- [CLI](#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 내장 객체와 Node의 `Buffer`만 사용할 수 있습니다. 스케줄링 함수(`setInterval`, `setTimeout`, `setImmediate`)는 기본적으로 사용할 수 없습니다.
**옵션:**
- `timeout` - 밀리초 단위의 스크립트 제한 시간. **경고**: 이 옵션은 `allowAsync=false`와 함께 사용하는 것이 좋습니다. 또한 샌드박스에서 반환된 객체를 조작하면 임의의 코드가 실행되어 제한 시간을 우회할 수 있습니다. 반환된 객체가 `typeof`로 원시 값인지 확인하고, 그렇지 않은 경우 해당 객체를 완전히 폐기해야 합니다(해당 객체로 로깅하거나 오류 메시지를 생성하는 것도 다시 임의의 코드를 실행할 수 있습니다).
- `sandbox` - VM의 전역 객체.
- `compiler` - `javascript`(기본값), `typescript`, `coffeescript` 또는 사용자 정의 컴파일러 함수. 값이 `typescript` 또는 `coffeescript`로 설정된 경우 컴파일러가 사전 설치되어 있어야 합니다. **`typescript`는 `typescript@6` 이하가 필요합니다** — [컴파일러](#compilers) 참조.
- `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)에서 신뢰할 수 없는 코드를 실행하는 임베더는 `timeout`을 선택하는 것과 같은 방식으로 계층적 DoS 방어의 일환으로 유한한 상한(예: `32 * 1024 * 1024`)을 선택해야 합니다. 아래 [강화 권장 사항](#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 컨텍스트에서와 동일한 방식으로 모듈을 require할 수 있게 해줍니다.
**옵션:**
- `console` - `inherit`는 콘솔을 활성화하고, `redirect`는 이벤트로 리디렉션하며, `off`는 콘솔을 비활성화합니다 (기본값: `inherit`).
- `sandbox` - VM의 전역 객체.
- `compiler` - `javascript` (기본값), `typescript`, `coffeescript` 또는 사용자 정의 컴파일러 함수 (코드와 해당 파일 경로를 받음). 값이 `typescript` 또는 `coffeescript`로 설정된 경우 라이브러리는 컴파일러가 사전 설치되어 있다고 가정합니다. **`typescript`는 `typescript@6` 이하가 필요합니다** — [컴파일러](#compilers) 참조.
- `eval` - `false`로 설정하면 `eval` 또는 함수 생성자(`Function`, `GeneratorFunction` 등)에 대한 모든 호출이 `EvalError`를 발생시킵니다 (기본값: `true`).
- `wasm` - `false`로 설정하면 WebAssembly 모듈을 컴파일하려는 모든 시도가 `WebAssembly.CompileError`를 발생시킵니다 (기본값: `true`). 참고: 보안상의 이유로 `WebAssembly.JSTag`는 샌드박스 내부에서 제거되므로 wasm 코드는 JavaScript 예외를 포착할 수 없습니다.
- `bufferAllocLimit` - `VM`과 동일한 의미 — 샌드박스 내부에서 발생하는 단일 `Buffer.alloc` 계열 요청의 최대 크기(바이트). 기본값: `Infinity`. [강화 권장 사항](#hardening-recommendations) 참조.
- `sourceExtensions` - 소스 코드로 처리할 파일 확장자 배열 (기본값: `['js']`).
- `require` - `require` 메서드를 활성화하려면 `true`, 객체 또는 Resolver (기본값: `false`).
- `require.external` - 값은 `true`, 허용된 외부 모듈 배열 또는 객체일 수 있습니다 (기본값: `false`). `/node_modules/${허용된_외부_모듈}/(?!/node_modules/)`와 일치하는 모든 경로는 require가 허용됩니다.
- `require.external.modules` - 허용된 외부 모듈 배열. 와일드카드도 지원하므로, 예를 들어 `['@scope/*-ver-??']`를 지정하면 `@scope/something-ver-aa`, `@scope/other-ver-11` 등의 형태의 이름을 가진 모든 모듈을 사용할 수 있습니다. `*` 와일드카드는 경로 구분자를 일치시키지 않습니다.
- `require.external.transitive` - 외부 모듈의 전이적 의존성이 허용되는지 여부를 나타내는 부울 값 (기본값: `false`). **경고**: 모듈이 전이적으로 require되면, 해당 모듈이 로드되기 전에는 불가능했더라도 이후에는 어떤 모듈이든 정상적으로 require할 수 있게 됩니다.
- `require.builtin` - 허용된 내장 모듈 배열, 전체 허용 시 `["\*"]` 허용 (기본값: 없음). **경고**: 새 내장 모듈이 추가될 수 있으므로 `"\*"`는 위험할 수 있습니다.
- `require.root` - 로컬 모듈을 require할 수 있는 제한된 경로 (기본값: 모든 경로).
- `require.mock` - 모의 모듈 모음 (외부 또는 내장).
- `require.context` - `host` (기본값)는 호스트에서 모듈을 require하고 샌드박스로 프록시합니다. `sandbox`는 샌드박스에서 모듈을 로드, 컴파일 및 require합니다. `callback(moduleFilename, ext)`는 모듈별로 컨텍스트를 동적으로 선택합니다. 아무것도 지정하지 않으면 기본값은 sandbox입니다. `events`를 제외하고 내장 모듈은 항상 호스트에서 require되어 샌드박스로 프록시됩니다.
- `require.import` - 시작 시 NodeVM에 로드할 모듈 배열.
- `require.resolve` - 기존의 node 조회 경로 중 하나에서 모듈을 찾지 못한 경우 사용할 추가 조회 함수.
- `require.customRequire` - 호스트에서 모듈을 로드하기 위해 `require` 함수 대신 사용.
- `require.strict` - `false`는 require로 로드된 모듈에 엄격 모드를 강제하지 않습니다 (기본값: `true`).
- `require.fs` - 사용자 정의 파일 시스템 구현.
- `nesting` - **경고**: 스크립트가 호스트 모듈을 require할 수 있는 NodeVM을 생성할 수 있으므로 이를 허용하는 것은 보안 위험입니다. `true`는 VM 중첩을 활성화합니다 (기본값: `false`).
- `wrapper` - `commonjs` (기본값)는 스크립트를 CommonJS 래퍼로 감싸고, `none`은 스크립트가 반환한 값을 검색합니다.
- `argv` - `process.argv`에 전달할 배열.
- `env` - `process.env`에 전달할 객체.
- `strict` - `true`는 로드된 모듈을 엄격 모드로 실행합니다 (기본값: `false`).
**중요**: 타임아웃은 NodeVM에 적용되지 않으므로 `while (true) {}` 또는 이와 유사한 악성 코드에 면역이 아닙니다.
**기억하세요**: 더 많은 모듈을 허용할수록 샌드박스는 더 취약해집니다.```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);
});
wrapper가 none으로 설정되면, NodeVM은 동기 코드에 대해 VM처럼 동작합니다.```js
assert.ok(vm.run('return true') === true);
**팁**: 더 많은 사용 예시는 테스트를 참조하세요.
### 상대 경로로 모듈 로드하기
상대 경로로 모듈을 로드하려면, 스크립트가 문자열인 경우 vm의 `run` 메서드에 두 번째 인자로 실행 중인 스크립트의 전체 경로를 전달해야 합니다. 그러면 해당 파일 이름이 스크립트에서 생성된 모든 스택 추적에 표시됩니다.```js
vm.run('require("foobar")', '/data/myvmscript.js');
If the script you are running is a VMScript, the path is given in the VMScript constructor.```js const script = new VMScript('require("foobar")', { filename: '/data/myvmscript.js' }); vm.run(script);
### Resolver
`makeResolverFromLegacyOptions`를 통해 resolver를 생성할 수 있으며, 여러 `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()을 사용하면 언제든지 코드를 컴파일할 수 있습니다. 코드가 컴파일된 후에는 이 메서드가 아무 효과가 없습니다.
compiler는 javascript(기본값), typescript, coffeescript 또는 사용자 정의 함수를 허용합니다. typescript 및 coffeescript 컴파일러는 선택 사항입니다. 패키지를 직접 설치하세요. vm2는 둘 중 어느 것에도 의존하지 않습니다.
내장된 typescript 컴파일러는 typescript@6 이하가 필요합니다.
vm2는 TypeScript의 transpileModule() API를 통해 트랜스파일합니다. TypeScript 7은 패키지 진입점에서 이를 제거했습니다. 해당 위치의 require('typescript')는 { version, versionMajorMinor }만으로 해석되며, 대체 API는 명시적으로 불안정한 typescript/unstable/* 하위 경로 뒤에 있으며, 그 중 어느 것도 단일 파일 트랜스파일과 동등한 기능을 제공하지 않습니다. 따라서 vm2가 7.x에서 대체할 수 있는 방법이 없습니다.
TypeScript 7이 설치된 상태에서 compiler: 'typescript'를 선택하면 new VMScript(...) / new VM(...)에서 오류가 발생합니다.```
VMError: The installed TypeScript (7.0.2) does not expose the transpileModule() API that
vm2's built-in TypeScript compiler uses; it was removed from the package entry point in
TypeScript 7. Install typescript@6 or earlier, or pass your own transpiler as a function:
{ compiler: (code, filename) => javaScriptSource }.
TypeScript 6을 고정하거나, 자체 트랜스파일러를 제공하세요 — JavaScript를 반환하는 함수라면 무엇이든 동작하므로, TypeScript 7의 `tsc`, esbuild, swc, 또는 타입 스트리퍼 모두 유효합니다:```js
import { VM, VMScript } from 'vm2';
import { transformSync } from 'esbuild';
const script = new VMScript('const x: number = 1; x', {
compiler: (code, filename) => transformSync(code, { loader: 'ts', format: 'cjs' }).code,
});
new VM().run(script);
A custom compiler receives (code, filename) and must return JavaScript source. It runs in the host realm, before sandboxing — treat it as trusted code and never build one out of untrusted input.
coffee-script 설치가 필요합니다. { header: false, bare: true } 옵션으로 컴파일되며, 전달하는 compilerOptions는 해당 옵션 위에 병합됩니다.
코드 컴파일 및 동기 코드 실행 중 발생하는 오류는 try-catch로 처리할 수 있습니다. 비동기 코드 실행 중 발생하는 오류는 Node의 process에 uncaughtException 이벤트 핸들러를 연결하여 처리할 수 있습니다.```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); });
## 샌드박스 코드 디버깅
샌드박스에서 실행되는 코드는 일반 프로세스에서 실행되는 것처럼 디버그하거나 검사할 수 있습니다.
- 중단점을 사용할 수 있습니다(스크립트 파일 이름을 지정해야 함).
- `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는 샌드박스 탈출(신뢰할 수 없는 코드가 호스트 영역 접근 권한을 획득하는 것)을 방지합니다. 그러나 그 자체만으로 모든 형태의 리소스 고갈 또는 서비스 거부(DoS)를 방지하지는 **않습니다**. 신뢰할 수 없는 코드를 실행하는 임베더는 샌드박스 주변에 다음과 같은 계층적 방어를 추가해야 합니다.
### 1. `bufferAllocLimit`으로 메모리 할당 제한
공격자가 제어하는 `N`을 사용한 단일 `Buffer.alloc(N)` 호출은 V8의 `timeout`이 중단할 수 없는 단일 동기 호스트 C++ 할당으로 실행됩니다. 메모리가 제한된 환경에서는 약 100바이트의 샌드박스 페이로드가 100MB 이상의 호스트 RSS 급증을 유발하고 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 핸들러 설치샌드박스 코드가 본문에서 스택 형식화 중 호스트 영역 오류를 트리거하는 값을 던지는 async function, async function* 또는 await using을 생성하는 호스트 프로세스 중단 DoS 클래스가 존재합니다(예: e.name = Symbol(); e.stack). V8은 영역의 내장 Promise를 통해 거부 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); });
애플리케이션에 처리되지 않은 거부(rejection)의 다른 원인이 없다면, 포괄적인 무시(swallow) + 로그 기록은 허용 가능합니다:```js
process.on('unhandledRejection', reason => {
yourLogger.warn('swallowed sandbox rejection', reason);
});
bufferAllocLimit을 설정하더라도, 호스트 프로세스를 워크로드에 맞는 --max-old-space-size(또는 이에 상응하는 컨테이너 메모리 제한)로 실행하세요. 이 상한은 단일 할당 프리미티브로부터 보호하며, OS 수준 제한은 총체적 고갈 및 vm2가 아직 제한하지 않은 향후 할당 프리미티브로부터 보호합니다.
require.builtin: ['*']를 비샌드박스 구성으로 취급'*' 와일드카드는 child_process, fs, dgram, net, http, dns를 포함한 대부분의 Node 내장 모듈로 확장됩니다. 이들은 완전한 호스트 기능 프리미티브입니다 — '*' 하에서 require('child_process').execSync('id')가 샌드박스에서 도달 가능합니다. vm2의 '*' 의미론은 의도적이지만(일부 임베더는 신뢰되지만 격리된 코드를 실행함), 신뢰할 수 없는 코드의 기본값으로 사용해서는 안 됩니다. 샌드박스가 실제로 필요로 하는 가장 작은 모듈 집합의 명시적 허용 목록을 선호하세요.
nesting: true는 탈출구입니다nesting: true는 샌드박스 코드가 require('vm2')를 호출하고 중첩된 NodeVM을 구성할 수 있게 합니다. 중첩된 VM의 require 구성은 이를 구성하는 샌드박스 코드에 의해 선택되며, 외부 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')`만 할 수 있고 다른 것은 할 수 없으며, 이는 정당한 용도가 없는 순수한 탈출 프리미티브입니다. 모든 require를 거부하려면 `nesting: true`를 제거하십시오. 중첩 VM을 허용하려면 명시적인 `require` 구성을 제공하여 호출 지점에서 그 절충점이 보이도록 하십시오.
## 알려진 문제점
- 프록시된 클래스를 상속하는 클래스를 정의할 수 없습니다. 여기에는 `Object.create`에서 프록시된 클래스를 사용하는 것도 포함됩니다.
- 직접 eval은 작동하지 않습니다.
- 샌드박스 배열을 로깅하면 속성에 배열 부분이 반복됩니다.
- 소스 코드 변환으로 인해 함수의 소스 문자열이 달라질 수 있습니다.
- 샌드박스 내부에서 node 프로세스를 중단시키는 방법이 있습니다. [강화 권장 사항](#hardening-recommendations)을 참조하십시오.
- 내장된 `typescript` 컴파일러는 TypeScript 7 이상과 호환되지 않습니다. TypeScript 7에서는 vm2가 사용하는 `transpileModule()` API가 제거되었기 때문입니다. `typescript@6`을 사용하거나 자체 트랜스파일러를 전달하십시오 — [컴파일러](#compilers)를 참조하십시오.
[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