ВАЖНО: Речь идет только о запуске кода JavaScript и TypeScript. При этом написание также может быть направлением для запуска другого кода на других языках.
Разрешение пользователям выполнять свой код в вашем приложении открывает мир настроек и функциональности, но при этом подвергает вашу платформу серьезным угрозам безопасности.
Учитывая, что это пользовательский код, ожидается все, от остановки серверов (это могут быть бесконечные циклы) до кражи конфиденциальной информации.
В этой статье будут рассмотрены различные стратегии по снижению риска запуска пользовательского кода, включая Web Workers, статический анализ кода и многое другое…
Существует множество сценариев, в которых вам необходимо запускать предоставленный пользователем код: от сред совместной разработки, таких как CodeSandbox и StackBiltz, до настраиваемых платформ API, таких как Январь. Даже игровые площадки с кодом подвержены рискам.
А именно, два существенных преимущества безопасного запуска предоставленного пользователем кода:
Запуск пользовательского кода не представляет опасности, пока вы не опасаетесь, что это может привести к краже некоторых данных. Какие бы данные вас ни беспокоили, они будут считаться конфиденциальной информацией. Например, в большинстве случаев JWT является конфиденциальной информацией (возможно, при использовании в качестве механизма аутентификации)
Учитывайте потенциальные риски, связанные с тем, что JWT хранится в файлах cookie, отправляемых с каждым запросом. Пользователь может случайно запустить запрос, который отправит 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 об eval
Выполнение временного интервала можно выполнить, запустив код в веб-воркере и используя 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(); }
Но он может получить доступ к глобальной области видимости, поэтому приведенный выше пример работает.
Вы можете запустить «Конструктор функций и eval» на WebWorker, что немного безопаснее из-за отсутствия доступа к DOM.
Чтобы ввести дополнительные ограничения, рассмотрите возможность запрета использования глобальных объектов, таких как fetch, XMLHttpRequest, sendBeacon. Прочтите эту статью о том, как это можно сделать.
Isolated-VM — это библиотека, которая позволяет запускать код на отдельной виртуальной машине (интерфейс Isolate 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")');
Этот код зарегистрирует 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 (браузер или node.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, среду выполнения контейнера, похожую на виртуальную машину, которая обеспечивает более безопасную среду. Sysbox того стоит, особенно если основное приложение работает в контейнере, а это значит, что вы будете запускать Docker в Docker.
Этот метод был выбран в январе, но вскоре возможности языка требовали большего, чем просто передача кода через оболочку контейнера. Кроме того, по какой-то причине память сервера часто перегружается; мы запускаем код внутри самоудаляющихся контейнеров при каждом нажатии клавиши в течение 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; }
Примечания:
Кроме того, вы можете вообще избежать транспиляции, запустив код с помощью Deno или Bun в контейнере докера, поскольку они поддерживают TypeScript «из коробки».
Запуск пользовательского кода — это палка о двух концах. Он может предоставить вашей платформе множество функций и возможностей настройки, но также подвергает вас значительным рискам безопасности. Очень важно понимать риски и принимать соответствующие меры для их смягчения, а также помнить, что чем более изолирована окружающая среда, тем она безопаснее.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3