重要: これは JavaScript および TypeScript コードの実行のみに関するものです。そうは言っても、この記述は、他のコードを他の言語で実行するための指示になる可能性もあります。
ユーザーがアプリケーション内でコードを実行できるようにすると、カスタマイズと機能の世界が開かれますが、プラットフォームが重大なセキュリティ上の脅威にさらされることにもなります。
これが user コードであることを考えると、サーバーの停止 (無限ループの可能性があります) から機密情報の窃取まで、すべてが予想されます。
この記事では、Web ワーカー、静的コード分析などを含む、ユーザー コードの実行を軽減するためのさまざまな戦略について説明します…
CodeSandbox や StackBiltz のような共同開発環境から、 January のようなカスタマイズ可能な API プラットフォームに至るまで、ユーザーが提供したコードを実行する必要があるシナリオは数多くあります。コードのプレイグラウンドでさえリスクにさらされます。
つまり、ユーザーが提供したコードを安全に実行することの 2 つの重要な利点は次のとおりです:
ユーザー コードの実行は、一部のデータが盗まれる可能性を懸念するまでは害はありません。あなたが懸念しているデータはすべて機密情報とみなされます。たとえば、ほとんどの場合、JWT は機密情報です (おそらく認証メカニズムとして使用される場合)
リクエストごとに送信される Cookie に保存されている 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();
このコードは、グローバル スコープで定義された fetch を使用します。インタプリタはそれについて知りませんが、eval はウィンドウにアクセスできるため、知っています。これは、ブラウザでの eval の実行は、サーバー環境やワーカーでの実行とは異なることを意味します。
eval(`document.body`);
これはどう...
eval(`while (true) {}`);
このコードはブラウザのタブを停止します。なぜユーザーが自分自身にこのようなことをするのか疑問に思うかもしれません。そうですね、彼らはインターネットからコードをコピーしているのかもしれません。そのため、実行をタイムボックス化して静的分析を行うことが好ましいのです。
eval に関する MDN ドキュメントを確認するとよいでしょう
タイム ボックスの実行は、Web ワーカーでコードを実行し、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 を記録します。
注: 2 番目の引数は関数本体です。
関数コンストラクターは外側のスコープにアクセスできないため、次のコードはエラーをスローします。
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
ただし、グローバル スコープにアクセスできるため、上記のフェッチ例は機能します。
WebWorker で「Function Constructor と eval を実行できます。これは、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 を記録します
これは、コードを実行するためのサンドボックス環境を提供するため、興味深いオプションです。注意点の 1 つは、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 ランタイム (ブラウザーまたは 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 を使用してコードを解析し、悪意のある行為がないかチェックできます。
そうですね、同じ話ですが、(オプションの場合もあります) 追加の手順が 1 つあります。コードを実行する前に 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; }
ノート:
さらに、Docker コンテナで Deno または Bun を使用してコードを実行すると、トランスパイルを完全に回避できます。これらはすぐに TypeScript をサポートしているためです。
ユーザー コードの実行は諸刃の剣です。プラットフォームに多くの機能とカスタマイズを提供できますが、重大なセキュリティ リスクにもさらされます。リスクを理解し、リスクを軽減するための適切な措置を講じることが重要です。また、環境が隔離されていればいるほど安全であることを忘れないでください。
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3