IMPORTANT : Il s'agit uniquement d'exécuter du code JavaScript et TypeScript. Cela étant dit, l'écriture peut également être la direction pour exécuter d'autres codes dans d'autres langages.
Permettre aux utilisateurs d'exécuter leur code dans votre application ouvre un monde de personnalisation et de fonctionnalités, mais cela expose également votre plate-forme à des menaces de sécurité importantes.
Étant donné qu'il s'agit de code utilisateur, tout est attendu, de l'arrêt des serveurs (il pourrait s'agir de boucles infinies) au vol d'informations sensibles.
Cet article explorera diverses stratégies pour atténuer l'exécution du code utilisateur, notamment les Web Workers, l'analyse de code statique, etc.…
Il existe de nombreux scénarios dans lesquels vous devez exécuter du code fourni par l'utilisateur, allant des environnements de développement collaboratifs tels que CodeSandbox et StackBiltz aux plates-formes API personnalisables comme January. Même les terrains de jeux de code sont sensibles aux risques.
À savoir, les deux avantages essentiels de l'exécution en toute sécurité du code fourni par l'utilisateur sont :
L'exécution de code utilisateur n'est pas dangereuse jusqu'à ce que vous craigniez que cela puisse entraîner le vol de certaines données. Toutes les données qui vous préoccupent seront considérées comme des informations sensibles. Par exemple, dans la plupart des cas, JWT constitue une information sensible (peut-être lorsqu'elle est utilisée comme mécanisme d'authentification)
Considérez les risques potentiels de JWT stocké dans les cookies envoyés à chaque demande. Un utilisateur pourrait par inadvertance déclencher une requête qui envoie le JWT à un serveur malveillant, et...
Le plus simple de tous, mais le plus risqué.
eval('console.log("I am dangerous!")');
Lorsque vous exécutez ce code, il enregistre ce message. Essentiellement, eval est un interpréteur JS capable d'accéder à la portée globale/fenêtre.
const res = await eval('fetch(`https://jsonplaceholder.typicode.com/users`)'); const users = await res.json();
Ce code utilise fetch qui est défini dans la portée globale. L’interprète ne le sait pas, mais comme eval peut accéder à une fenêtre, il le sait. Cela implique que l'exécution d'une évaluation dans le navigateur est différente de son exécution dans un environnement serveur ou dans un environnement de travail.
eval(`document.body`);
Que dis-tu de ça...
eval(`while (true) {}`);
Ce code arrêtera l'onglet du navigateur. Vous pourriez vous demander pourquoi un utilisateur se ferait cela. Eh bien, ils copient peut-être du code sur Internet. C'est pourquoi il est préférable d'effectuer une analyse statique avec/ou de limiter le temps d'exécution.
Vous souhaiterez peut-être consulter MDN Docs à propos d'eval
L'exécution de la boîte de temps peut être effectuée en exécutant le code dans un Web Worker et en utilisant setTimeout pour limiter le temps d'exécution.
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) {}');
Ceci est similaire à eval mais c'est un peu plus sûr car il ne peut pas accéder à la portée englobante.
const userFunction = new Function('param', 'console.log(param);'); userFunction(2);
Ce code enregistrera 2.
Remarque : Le deuxième argument est le corps de la fonction.
Le constructeur de fonction ne peut pas accéder à la portée englobante, de sorte que le code suivant génère une erreur.
function fnConstructorCannotUseMyScope() { let localVar = 'local value'; const userFunction = new Function('return localVar'); return userFunction(); }
Mais il peut accéder à la portée globale afin que l'exemple de récupération ci-dessus fonctionne.
Vous pouvez exécuter « Function Constructor et eval sur un WebWorker, ce qui est un peu plus sûr car il n'y a pas d'accès au DOM.
Pour mettre plus de restrictions en place, envisagez d'interdire l'utilisation d'objets globaux tels que fetch, XMLHttpRequest, sendBeacon. Consultez cet écrit pour savoir comment procéder.
Isolated-VM est une bibliothèque qui vous permet d'exécuter du code dans une VM distincte (interface Isolate de la 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")');
Ce code enregistrera Hello World
Il s'agit d'une option intéressante car elle fournit un environnement en bac à sable pour exécuter du code. Une mise en garde est que vous avez besoin d'un environnement avec des liaisons Javascript. Cependant, un projet intéressant appelé Extisme facilite cela. Vous voudrez peut-être suivre leur tutoriel.
Ce qui est fascinant, c'est que vous utiliserez eval pour exécuter le code, mais étant donné la nature de WebAssembly, le DOM, le réseau, le système de fichiers et l'accès à l'environnement hôte ne sont pas possibles (bien qu'ils puissent différer en fonction de le runtime 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 };
Vous devrez d'abord compiler le code ci-dessus à l'aide d'Extism, qui générera un fichier Wasm pouvant être exécuté dans un environnement doté d'un runtime Wasm (navigateur ou 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
Nous passons maintenant au côté serveur, Docker est une excellente option pour exécuter du code indépendamment de la machine hôte. (Méfiez-vous de l'évasion du conteneur)
Vous pouvez utiliser dockerode pour exécuter le code dans un conteneur.
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'], }, });
Gardez à l'esprit que vous devez vous assurer que Docker est installé et exécuté sur le serveur. Je recommanderais d'avoir un serveur distinct dédié uniquement à cela qui agit comme un serveur purement fonctionnel.
De plus, vous pourriez bénéficier de jeter un œil à sysbox, un environnement d'exécution de conteneur de type VM qui fournit un environnement plus sécurisé. Sysbox en vaut la peine, surtout si l'application principale s'exécute dans un conteneur, ce qui signifie que vous exécuterez Docker dans Docker.
C'était la méthode de choix en janvier, mais assez vite, les capacités du langage imposaient plus que simplement transmettre le code via le shell du conteneur. De plus, pour une raison quelconque, la mémoire du serveur augmente fréquemment ; nous exécutons le code dans des conteneurs auto-amovibles toutes les 1 secondes de frappe anti-rebond. (Tu peux faire mieux!)
J'aime particulièrement Firecracker, mais c'est un peu de travail à mettre en place, donc si vous n'avez pas encore le temps, vous voulez être prudent, faites une combinaison d'analyse statique et d'exécution de time-boxing. . Vous pouvez utiliser Esprima pour analyser le code et rechercher tout acte malveillant.
Eh bien, même histoire avec une étape supplémentaire (peut-être facultative) : transpiler le code en JavaScript avant de l'exécuter. En termes simples, vous pouvez utiliser le compilateur esbuild ou typescript, puis continuer avec les méthodes ci-dessus.
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; }
Remarques:
De plus, vous pouvez éviter complètement la transpilation en exécutant le code à l'aide de Deno ou Bun dans un conteneur Docker, car ils prennent en charge TypeScript dès le départ.
L'exécution du code utilisateur est une arme à double tranchant. Il peut apporter de nombreuses fonctionnalités et personnalisations à votre plateforme, mais il vous expose également à des risques de sécurité importants. Il est essentiel de comprendre les risques et de prendre les mesures appropriées pour les atténuer et de se rappeler que plus l'environnement est isolé, plus il est sûr.
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3