중요: 이것은 JavaScript 및 TypeScript 코드 실행에만 해당됩니다. 즉, 그 글은 다른 언어로 다른 코드를 실행하는 방향이 될 수도 있습니다.
사용자가 애플리케이션 내에서 코드를 실행할 수 있도록 허용하면 사용자 정의 및 기능의 세계가 열리지만 동시에 플랫폼이 심각한 보안 위협에 노출됩니다.
사용자 코드인 점을 감안하면 서버 정지(무한 루프일 수 있음)부터 민감한 정보 도용까지 모든 것이 예상됩니다.
이 기사에서는 웹 작업자, 정적 코드 분석 등을 포함하여 실행 사용자 코드를 완화하기 위한 다양한 전략을 살펴보겠습니다...
CodeSandbox 및 StackBiltz와 같은 공동 개발 환경부터 January와 같은 사용자 정의 가능한 API 플랫폼에 이르기까지 사용자 제공 코드를 실행해야 하는 시나리오는 많습니다. 코드 플레이그라운드도 위험에 취약합니다.
즉, 사용자 제공 코드를 안전하게 실행하는 두 가지 필수 이점은 다음과 같습니다.
사용자 코드를 실행하는 것은 일부 데이터가 도난당할 수 있다는 우려가 있기 전까지는 해롭지 않습니다. 귀하가 우려하는 모든 데이터는 민감한 정보로 간주됩니다. 예를 들어 대부분의 경우 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) {}');
이것은 eval과 비슷하지만 바깥쪽 범위에 접근할 수 없기 때문에 좀 더 안전합니다.
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(); }
하지만 전역 범위에 액세스할 수 있으므로 위의 가져오기 예시가 작동합니다.
WebWorker에서 "함수 생성자 및 평가"를 실행할 수 있는데, 이는 DOM 액세스가 없기 때문에 조금 더 안전합니다.
더 많은 제한을 적용하려면 fetch, XMLHttpRequest, sendBeacon과 같은 전역 개체 사용을 허용하지 않는 것이 좋습니다. 이를 수행하는 방법에 대해서는 이 글을 확인하세요.
Isolated-VM은 별도의 VM(v8의 Isolate 인터페이스)에서 코드를 실행할 수 있는 라이브러리입니다.
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")');
이 코드는 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-runtime(브라우저 또는 node.js)이 있는 환경에서 실행될 수 있는 Wasm 파일이 출력됩니다.
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'], }, });
서버에 Docker가 설치되어 실행 중인지 확인해야 합니다. 순수 기능 서버 역할을 하는 전용 서버를 별도로 두는 것이 좋습니다.
또한 보다 안전한 환경을 제공하는 VM과 유사한 컨테이너 런타임인 sysbox를 살펴보는 것이 좋습니다. Sysbox는 그만한 가치가 있습니다. 특히 기본 앱이 컨테이너에서 실행되는 경우, 즉 Docker에서 Docker를 실행한다는 의미입니다.
이것은 1월에 선택한 방법이었지만 얼마 지나지 않아 언어 기능은 컨테이너 셸을 통해 코드를 전달하는 것 이상의 것을 요구하게 되었습니다. 게다가 어떤 이유로든 서버 메모리가 자주 급증합니다. 우리는 디바운싱된 키 입력이 1초마다 자체 제거 가능한 컨테이너 내에서 코드를 실행합니다. (넌 더 잘할 수있어!)
저는 특히 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; }
노트:
또한, 기본적으로 TypeScript를 지원하므로 Docker 컨테이너에서 Deno 또는 Bun을 사용하여 코드를 실행하면 트랜스파일을 완전히 피할 수 있습니다.
사용자 코드 실행은 양날의 검입니다. 플랫폼에 많은 기능과 사용자 정의를 제공할 수 있지만 심각한 보안 위험에 노출되기도 합니다. 위험을 이해하고 이를 완화하기 위한 적절한 조치를 취하는 것이 중요합니다. 환경이 더 격리될수록 더 안전하다는 점을 기억하세요.
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3