IMPORTANTE: Se trata de ejecutar código JavaScript y TypeScript únicamente. Dicho esto, la escritura también podría ser la dirección para ejecutar otro código en otros idiomas.
Permitir que los usuarios ejecuten su código dentro de su aplicación abre un mundo de personalización y funcionalidad, pero también expone su plataforma a importantes amenazas de seguridad.
Dado que es código de usuario, se espera de todo, desde detener los servidores (podrían ser bucles infinitos) hasta robar información confidencial.
Este artículo explorará varias estrategias para mitigar la ejecución de código de usuario, incluidos Web Workers, análisis de código estático y más…
Existen muchos escenarios en los que es necesario ejecutar código proporcionado por el usuario, desde entornos de desarrollo colaborativo como CodeSandbox y StackBiltz hasta plataformas API personalizables como January. Incluso los campos de juego de código son susceptibles a riesgos.
Es decir, las dos ventajas esenciales de ejecutar de forma segura el código proporcionado por el usuario son:
Ejecutar un código de usuario no es perjudicial a menos que te preocupe que esto pueda provocar el robo de algunos datos. Cualquier dato que le preocupe se considerará información confidencial. Por ejemplo, en la mayoría de los casos, JWT es información confidencial (quizás cuando se usa como mecanismo de autenticación)
Considere los riesgos potenciales de JWT almacenado en las cookies enviadas con cada solicitud. Un usuario podría activar sin darse cuenta una solicitud que envíe el JWT a un servidor malicioso y...
El más simple de todos, pero el más arriesgado.
eval('console.log("I am dangerous!")');
Cuando ejecuta este código, registra ese mensaje. Básicamente, eval es un intérprete JS capaz de acceder al alcance global/de ventana.
const res = await eval('fetch(`https://jsonplaceholder.typicode.com/users`)'); const users = await res.json();
Este código utiliza la recuperación que se define en el ámbito global. El intérprete no lo sabe, pero como eval puede acceder a una ventana, lo sabe. Eso implica que ejecutar una evaluación en el navegador es diferente a ejecutarla en un entorno de servidor o trabajador.
eval(`document.body`);
Qué tal esto...
eval(`while (true) {}`);
Este código detendrá la pestaña del navegador. Quizás se pregunte por qué un usuario se haría esto a sí mismo. Bueno, es posible que estén copiando código de Internet. Es por eso que se prefiere hacer análisis estático con/o cronometrar la ejecución.
Es posible que desees consultar MDN Docs sobre la evaluación
La ejecución del cuadro de tiempo se puede realizar ejecutando el código en un trabajador web y usando setTimeout para limitar el tiempo de ejecución.
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) {}');
Esto es similar a eval pero es un poco más seguro ya que no puede acceder al alcance adjunto.
const userFunction = new Function('param', 'console.log(param);'); userFunction(2);
Este código registrará 2.
Nota: El segundo argumento es el cuerpo de la función.
El constructor de la función no puede acceder al alcance adjunto, por lo que el siguiente código arrojará un error.
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
Pero puede acceder al alcance global, por lo que el ejemplo de búsqueda anterior funciona.
Puedes ejecutar “Constructor de funciones y evaluación en un WebWorker, lo cual es un poco más seguro debido al hecho de que no hay acceso DOM.
Para implementar más restricciones, considere no permitir el uso de objetos globales como fetch, XMLHttpRequest, sendBeacon. Consulte este escrito sobre cómo puede hacerlo.
Isolated-VM es una biblioteca que le permite ejecutar código en una VM separada (interfaz Isolate de 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")');
Este código registrará hola mundo
Esta es una opción interesante ya que proporciona un entorno aislado para ejecutar código. Una advertencia es que necesita un entorno con enlaces de Javascript. Sin embargo, un interesante proyecto llamado Extism lo facilita. Quizás quieras seguir su tutorial.
Lo fascinante de esto es que usarás eval para ejecutar el código, pero dada la naturaleza de WebAssembly, el DOM, la red, el sistema de archivos y el acceso al entorno host no son posibles (aunque pueden diferir según el el tiempo de ejecución de 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 };
Primero tendrás que compilar el código anterior usando Extism, lo que generará un archivo Wasm que se puede ejecutar en un entorno que tenga Wasm-runtime (navegador o 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
Ahora nos estamos moviendo al lado del servidor, Docker es una excelente opción para ejecutar código de forma aislada de la máquina host. (Cuidado con la fuga de contenedores)
Puedes usar Dockerode para ejecutar el código en un contenedor.
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'], }, });
Tenga en cuenta que debe asegurarse de que el servidor tenga Docker instalado y ejecutándose. Recomendaría tener un servidor separado dedicado solo a esto que actúe como un servidor de funciones puras.
Además, podría resultarle beneficioso echar un vistazo a sysbox, un tiempo de ejecución de contenedor similar a una máquina virtual que proporciona un entorno más seguro. Sysbox vale la pena, especialmente si la aplicación principal se ejecuta en un contenedor, lo que significa que ejecutará Docker en Docker.
Este fue el método elegido en enero, pero muy pronto, las capacidades del lenguaje exigían algo más que pasar el código a través del shell del contenedor. Además, por alguna razón, la memoria del servidor aumenta con frecuencia; ejecutamos el código dentro de contenedores autoextraíbles cada vez que se pulsa una tecla sin rebote. (¡Puedes hacerlo mejor!)
Me gusta especialmente Firecracker, pero su configuración requiere un poco de trabajo, por lo que si aún no puedes permitirte el tiempo, quieres estar seguro, haz una combinación de análisis estático y ejecución de time-boxing. . Puede utilizar esprima para analizar el código y comprobar si hay algún acto malicioso.
Bueno, la misma historia con un paso adicional (podría ser opcional): transpilar el código a JavaScript antes de ejecutarlo. En pocas palabras, puedes usar el compilador esbuild o mecanografiado y luego continuar con los métodos anteriores.
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; }
Notas:
Además, puedes evitar la transpilación por completo ejecutando el código usando Deno o Bun en un contenedor acoplable, ya que admiten TypeScript desde el primer momento.
Ejecutar código de usuario es un arma de doble filo. Puede proporcionar mucha funcionalidad y personalización a su plataforma, pero también lo expone a importantes riesgos de seguridad. Es fundamental comprender los riesgos y tomar las medidas adecuadas para mitigarlos y recordar que cuanto más aislado esté el entorno, más seguro será.
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3