12k
All articles

Le modèle de permissions de Deno expliqué

Permissions Deno expliquées : flags allow et deny, portée, ensembles de permissions deno.json, API Permissions et pièges de sécurité.

OpenReplay Team
OpenReplay Team
Le modèle de permissions de Deno expliqué

Deno exécute votre code dans un bac à sable qui n’accorde rien au départ : le système de fichiers, le réseau, les variables d’environnement, les sous-processus, les informations système et les bibliothèques natives (FFI) sont tous fermés jusqu’à ce que vous activiez chacun d’eux avec un indicateur --allow-*, et presque chaque indicateur accepte un argument qui restreint l’autorisation à des chemins, hôtes ou variables nommés.

Si vous venez de Node, votre premier script Deno s’arrêtera très probablement net sur une erreur de permission, et la solution est rarement le -A global vers lequel la mémoire musculaire nous pousse. Déterminer quel indicateur ajouter, et avec quelle granularité le restreindre, constitue l’essentiel de la courbe d’apprentissage.

C’est l’inverse du comportement historique par défaut de Node.js, et c’est la chose la plus importante à comprendre avant d’exécuter le moindre script Deno. Cet article détaille ce qui est bloqué par défaut, chaque indicateur --allow-* et --deny-* avec sa syntaxe de restriction, les ajouts de Deno 2.x (priorité du refus, --allow-sys, --allow-import, jokers pour l’environnement), les ensembles de permissions dans deno.json, l’API Permissions à l’exécution, ainsi que les deux pièges de sécurité que la documentation évoque le plus discrètement.

Points clés à retenir

  • Par défaut, le code Deno ne peut ni lire ni écrire de fichiers, ouvrir des connexions réseau, lire des variables d’environnement, lancer des sous-processus, accéder aux informations système ou charger des bibliothèques natives. Vous activez chaque capacité individuellement avec un indicateur --allow-*.
  • Chaque indicateur --allow-* possède un équivalent --deny-*, et le refus l’emporte toujours : --allow-read=. --deny-read=./secrets accorde l’accès au répertoire du projet tout en maintenant ./secrets illisible.
  • Dans Deno 2, une capacité refusée déclenche Deno.errors.NotCapable (renommée depuis l’ancienne PermissionDenied), distinguant les refus de permission propres à Deno des erreurs système ordinaires.
  • Depuis Deno 2.5, vous pouvez définir des ensembles de permissions nommés dans deno.json et les appliquer avec -P=name (ou un ensemble default avec un simple -P), ce qui permet de versionner des indicateurs respectant le principe du moindre privilège.
  • Rien dans le graphe initial d’imports statiques n’est vérifié par le système de permissions avant son chargement, et --allow-run exécute des sous-processus en dehors du bac à sable. Ce sont les deux façons dont du code non fiable peut s’échapper.

Pourquoi Deno est-il sécurisé par défaut ?

Rien ne s’exécute avec des privilèges ambiants : disque, réseau, environnement et lancement de sous-processus restent fermés jusqu’à ce que vous les ouvriez. Cette décision de conception vient directement de Ryan Dahl, le créateur original de Node, qui a conçu Deno pour inverser le comportement par défaut de Node, à savoir « l’accès complet à tout ». Dans Deno, les dépendances ne disposent d’aucune autorité ambiante propre ; dans Node, un paquet hérite de toutes les entrées/sorties système accessibles au processus englobant, et cet écart constitue la différence la plus marquante entre les deux environnements d’exécution.

Node a depuis ajouté son propre modèle de permissions. Il a été livré à titre expérimental dans Node 20 derrière --experimental-permission, a été marqué stable en v23.5.0, et Node 24 a supprimé l’orthographe expérimentale au profit du simple --permission. Le modèle de Deno reste plus abouti : il est activé par défaut plutôt que par un indicateur optionnel, et il couvre davantage de classes de capacités avec une granularité plus fine.

Que sont les indicateurs de permission —allow-* de Deno ?

Chaque capacité correspond à un indicateur, et la plupart des indicateurs acceptent un argument de liste blanche. Un indicateur nu accorde tout dans cette classe ; un argument le restreint. Un simple --allow-net accorde l’accès à tous les hôtes sur tous les ports, tandis que --allow-net=api.example.com:443 limite le programme à exactement un hôte et un port.

IndicateurProtègeExemple restreintÉquivalent de refus
--allow-readLectures du système de fichiers--allow-read=./data,config.ini--deny-read
--allow-writeÉcritures du système de fichiers--allow-write=./tmp--deny-write
--allow-netAccès réseau--allow-net=api.example.com:443--deny-net
--allow-envVariables d’environnement--allow-env=PORT,HOST--deny-env
--allow-runSous-processus--allow-run=git,deno--deny-run
--allow-sysAPI d’informations système--allow-sys=hostname--deny-sys
--allow-ffiBibliothèques natives--allow-ffi=./lib.so--deny-ffi
--allow-importImports HTTPS distants--allow-import=jsr.io--deny-import

Notez qu’il n’existe pas de --allow-hrtime. Cet indicateur a été supprimé dans Deno 2.0, et les API de chronométrage haute résolution comme performance.now() sont désormais toujours disponibles.

Lorsqu’un script a besoin d’une permission que vous n’avez pas accordée, Deno met en pause l’exécution et affiche une invite interactive :

┏ ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

Répondez y pour accorder une seule fois, n pour refuser (ce qui déclenche Deno.errors.NotCapable), ou A pour autoriser toute la classe. En CI, passez les indicateurs dès le départ afin que rien ne se bloque sur une invite.

Comment l’ensemble d’indicateurs s’est étoffé : priorité du refus, --allow-sys, --allow-import, jokers pour l’environnement

Les indicateurs de refus sont arrivés dans Deno 1.36 (août 2023), et chaque indicateur --allow-* possède depuis un équivalent --deny-*. Partout où les deux se recoupent, c’est le refus qui s’applique, ce qui permet d’accorder largement tout en ménageant des exceptions :

deno run --allow-read=. --deny-read=./secrets app.ts

--allow-sys, qui remonte à Deno 1.26 (octobre 2022), contrôle les API d’informations système telles que Deno.hostname() et Deno.systemMemoryInfo(). La seule classe de capacité véritablement nouvelle dans Deno 2.0 était --allow-import, qui régit les hôtes HTTPS depuis lesquels votre code peut récupérer des modules à l’exécution ; le HTTP simple n’est jamais autorisé, les imports statiques sont filtrés automatiquement par rapport à la liste, et nommer vos propres hôtes remplace l’ensemble intégré de Deno au lieu de s’y ajouter. Utilisez --deny-import pour bloquer purement et simplement des hôtes spécifiques.

L’accès à l’environnement a gagné les jokers de suffixe dans Deno 2.1. Au lieu d’énumérer chaque variable, restreignez par préfixe :

deno run --allow-env="AWS_*" main.ts

Déclarer les permissions dans deno.json

Depuis Deno 2.5, vous pouvez définir des ensembles de permissions nommés dans deno.json et les appliquer avec -P=name (ou --permission-set=name), ce qui permet de versionner les indicateurs de moindre privilège au lieu de les retaper à chaque exécution. Les clés de l’objet sont les noms des indicateurs (read, write, net, env, sys, run, ffi, import), comme documenté dans la référence deno.json :

{
  "permissions": {
    "default": {
      "read": ["./deno.json"],
      "env": true,
      "run": { "allow": ["git"] }
    },
    "process-data": {
      "read": ["./data"],
      "write": ["./data"]
    }
  },
  "tasks": {
    "dev": "deno run -P main.ts"
  }
}

Exécutez deno run -P=process-data main.ts pour l’ensemble nommé, ou deno run -P main.ts pour l’ensemble default. Deno 2.5 a également ajouté la variable d’environnement DENO_AUDIT_PERMISSIONS : pointez-la vers un chemin de fichier et Deno y ajoute une entrée JSONL pour chaque permission sollicitée par le programme, que cet accès ait été accordé ou refusé. C’est un moyen rapide de découvrir ce dont un script a réellement besoin.

L’API Permissions à l’exécution

Interrogez les permissions dans le code avant une opération restreinte afin d’échouer proprement plutôt que de planter sur une erreur NotCapable. Deno.permissions expose query, request et revoke, chacune acceptant un descripteur tel que { name: "net", host: "example.com" } :

const desc = { name: "net", host: "example.com" } as const;

let status = await Deno.permissions.query(desc); // "prompt" | "granted" | "denied"
if (status.state === "prompt") {
  status = await Deno.permissions.request(desc); // triggers the y/n/A prompt
}

if (status.state === "granted") {
  await fetch("https://example.com");
}

await Deno.permissions.revoke(desc); // drop it again

query renvoie l’état actuel sans afficher d’invite, request sollicite l’utilisateur si l’état est encore prompt, et revoke restitue une capacité. Cela permet aux programmes de longue durée de vérifier avant d’accéder à une ressource et d’emprunter un autre chemin lorsque l’accès n’est pas disponible.

Les deux pièges : les imports et --allow-run

Deux comportements permettent à du code non fiable de contourner le bac à sable, et il vaut la peine de bien les assimiler. Premièrement, tout ce que Deno peut résoudre statiquement depuis votre point d’entrée (fichiers locaux, paquets npm et JSR, et URL distantes écrites en littéraux de chaîne) est récupéré avant que le système de permissions n’ait son mot à dire ; ainsi, une dépendance peut lire son propre code source et atteindre le réseau avant même que votre premier indicateur --allow-* ne s’applique. Ce laissez-passer ne concerne que le chargement, rien de plus : dès que le code s’exécute, chaque opération est de nouveau vérifiée. --allow-import restreint quels hôtes distants peuvent être importés, mais ne fait pas en sorte que les imports eux-mêmes nécessitent une autorisation à l’exécution ; auditez donc le code tiers avant de l’intégrer.

Deuxièmement, --allow-run est le piège le plus redoutable : tout ce que vous lancez devient un processus à part entière, portant les privilèges que le système d’exploitation lui accorde plutôt que l’ensemble restreint que vous avez confié à Deno. Cela signifie que --allow-run=deno permet à un script en bac à sable de relancer Deno avec --allow-all et de s’échapper complètement. Il ne restreint par ailleurs que l’exécutable lancé, pas ses arguments : --allow-run=cat permet au code de lire n’importe quel fichier via cat. Limitez-le à des binaires de confiance spécifiques comme --allow-run=git, et notez que --allow-ffi comporte la même catégorie de risque, puisque les bibliothèques natives s’exécutent en code machine, en dehors des vérifications de la couche JavaScript.

La posture pratique : accordez la liste blanche la plus étroite qui fonctionne, ajoutez des --deny-* sur les chemins sensibles, et traitez --allow-run et --allow-ffi comme des frontières de confiance, non comme des commodités. Partez de zéro permission, exécutez le script, et rajoutez exactement ce que les invites (ou le journal DENO_AUDIT_PERMISSIONS) vous indiquent comme nécessaire.

FAQ

Quelle est la différence entre répondre non à une invite de permission Deno et Deno.errors.NotCapable ?

Il s'agit du même résultat, atteint par des chemins différents. Lorsque vous répondez « n » à une invite interactive, ou que vous exécutez le programme sans l'indicateur requis, l'opération refusée déclenche Deno.errors.NotCapable dans Deno 2 (renommée depuis l'ancienne PermissionDenied). Ce changement de nom permet de distinguer les refus de permission propres à Deno des erreurs ordinaires du système d'exploitation, comme un fichier manquant, puisque les deux se manifestaient auparavant par des échecs d'apparence similaire.

Est-ce que --allow-net=example.com autorise aussi le HTTPS sur le port 443 ?

Oui. Lorsque vous spécifiez un hôte sans port, comme --allow-net=example.com, Deno autorise les connexions vers cet hôte sur n'importe quel port, y compris le 443. Pour vous limiter à un seul port, vous devez l'écrire explicitement sous la forme --allow-net=example.com:443, ce qui bloque alors tous les autres ports de cet hôte. Un simple --allow-net sans argument accorde l'accès à tous les hôtes sur tous les ports.

Puis-je combiner --allow-read et --deny-read sur des chemins qui se recoupent ?

Oui, et le refus l'emporte toujours. Exécuter --allow-read=. --deny-read=./secrets accorde l'accès en lecture à l'ensemble du répertoire du projet à l'exception de ./secrets, qui reste illisible. Les indicateurs de refus ont priorité sur les indicateurs d'autorisation dans chaque classe de capacité ; ce modèle vous permet donc d'accorder largement tout en excluant les chemins sensibles, plutôt que d'énumérer individuellement chaque fichier autorisé.

Ai-je besoin de --allow-import pour utiliser des paquets npm ou JSR ?

Non, pas pour les paquets importés statiquement. Tout ce que Deno peut résoudre depuis votre point d'entrée sans exécuter de code, y compris les paquets npm et JSR, est récupéré avant que le système de permissions ne soit consulté. --allow-import détermine de quels hôtes HTTPS les imports distants peuvent provenir, et le HTTP simple n'est jamais une option. Un spécificateur calculé à l'exécution, c'est différent : une URL distante dynamique nécessite --allow-import, et un chemin local dynamique nécessite --allow-read.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.