Présentation du modèle de permissions
Deno se définit comme un runtime JavaScript/TypeScript reposant sur le moteur V8. Son argument commercial principal est le mode "Secure by default" : chaque appel à la bibliothèque standard nécessite une permission explicite, accordée au lancement (deno run --allow-read=/tmp/dir --allow-run=uname script.ts) ou à la demande via Deno.permissions.request(). Le prompt affiché dans le terminal ressemble à :
┌ ⚠️ Deno requests read access to "/tmp/dir".
├ Requested by `Deno.permissions.request()` API.
├ Run again with --allow-read to bypass this prompt.
└ Allow? [y/n/A] >
Cette approche suppose que le terminal agit comme un canal fiable pour les interactions de sécurité.
Problèmes liés aux invites en terminal
Le terminal communique exclusivement via stdout et stdin. Deno ne filtre ni ne restreint l’écriture sur stdout ni la lecture sur stdin, même lorsqu’une permission n’est pas accordée. Un script malveillant peut donc injecter du texte qui modifie l’apparence du prompt (par ex. en changeant les couleurs) ou qui envoie automatiquement une réponse à stdin. L’auteur montre un enregistrement où le code masque le prompt et fournit une validation silencieuse, démontrant que l’utilisateur ne voit jamais la demande réelle.
Cette situation est analogue à un site web qui demanderait un mot de passe sur une page déjà compromise : le canal d’entrée est contrôlé par l’attaquant, rendant impossible toute confiance dans la réponse de l’utilisateur. Même si les séquences d’échappement standards étaient corrigées, la diversité des terminaux (et leurs codes non‑standard) empêche une couverture exhaustive.
Vulnérabilités du flag --allow-run
Le flag --allow-run=uname ne désigne pas un binaire unique ; Deno parcourt les répertoires de PATH pour localiser le premier exécutable nommé uname. Un script peut donc redéfinir PATH à la volée :
const command = new Deno.Command("uname", {
env: { 'PATH': '/tmp/tmp.7ofmKaNW83' },
});
const { stdout } = command.outputSync();
console.log(new TextDecoder().decode(stdout));
En plaçant un script uname factice dans ce répertoire (#!/bin/sh\nprintf lol), l’exécution retourne lol malgré la permission limitée. Le problème s’étend aux exécutables dynamiquement liés : le chargeur dynamique lit des variables d’environnement telles que LD_PRELOAD, qui permettent d’injecter des bibliothèques partagées exécutant du code arbitraire avant même que le binaire ciblé ne démarre. Ainsi, même si --allow-run était limité à un chemin absolu, l’attaquant pourrait exploiter LD_PRELOAD pour contourner la restriction.
Conséquences et recommandations
Ces deux vecteurs – interaction terminale non isolée et résolution de chemin basée sur PATH combinée aux variables du chargeur dynamique – rendent le modèle de permissions de Deno intrinsèquement vulnérable. Un attaquant qui parvient à exécuter du code dans le runtime peut obtenir un accès effectif aux ressources système, même sans permission explicite.
Pour renforcer la sécurité, il faudrait :
• Isoler le prompt de permission dans un processus dédié, empêchant toute écriture concurrente sur le même flux.
• Exiger des chemins absolus ou des hachages de binaires pour --allow-run, et désactiver les variables d’environnement du chargeur dynamique lorsqu’une permission est accordée.
• Filtrer ou sandboxer les accès à stdout/stdin lorsqu’ils sont utilisés pour des dialogues de sécurité.
En l’état, le modèle de permissions de Deno ne garantit pas la séparation entre le code non fiable et les interactions de sécurité, ce qui contredit son slogan "Secure by default".