12k
All articles

El modelo de permisos de Deno, explicado

Permisos de Deno explicados: flags allow y deny, alcance, conjuntos en deno.json, API Permissions y riesgos de seguridad.

OpenReplay Team
OpenReplay Team
El modelo de permisos de Deno, explicado

Deno ejecuta tu código en un sandbox que no concede nada de entrada: el sistema de archivos, la red, las variables de entorno, los subprocesos, la información del sistema y las bibliotecas nativas (FFI) permanecen cerrados hasta que habilitas cada uno con una bandera --allow-*, y casi todas las banderas aceptan un argumento que acota la concesión a rutas, hosts o variables concretas.

Si vienes de Node, tu primer script en Deno se detendrá casi con toda seguridad con un error de permisos, y la solución rara vez es el -A general al que recurre la memoria muscular. Averiguar qué bandera añadir, y con qué alcance restringirla, constituye la mayor parte de la curva de aprendizaje.

Esto es lo inverso al comportamiento histórico por defecto de Node.js, y es lo más importante que hay que entender antes de ejecutar cualquier script de Deno. Este artículo desglosa qué está bloqueado por defecto, todas las banderas --allow-* y --deny-* con su sintaxis de alcance, las incorporaciones de Deno 2.x (precedencia de denegación, --allow-sys, --allow-import, comodines en variables de entorno), los conjuntos de permisos en deno.json, la API de permisos en tiempo de ejecución y los dos riesgos de seguridad sobre los que la documentación es más discreta.

Puntos clave

  • Por defecto, el código de Deno no puede leer ni escribir archivos, abrir conexiones de red, leer variables de entorno, generar subprocesos, acceder a información del sistema ni cargar bibliotecas nativas. Debes habilitar cada capacidad de forma explícita mediante una bandera --allow-*.
  • Cada bandera --allow-* tiene su contraparte --deny-*, y la denegación siempre prevalece: --allow-read=. --deny-read=./secrets concede el directorio del proyecto pero mantiene ./secrets ilegible.
  • En Deno 2, una capacidad denegada lanza Deno.errors.NotCapable (renombrado desde el antiguo PermissionDenied), lo que separa las denegaciones de permisos propias de Deno de los errores ordinarios del sistema operativo.
  • Desde Deno 2.5 puedes definir conjuntos de permisos con nombre en deno.json y aplicarlos con -P=nombre (o un conjunto default con un -P sin argumento), manteniendo las banderas de mínimo privilegio bajo control de versiones.
  • Nada del grafo de importaciones estáticas inicial se verifica contra el sistema de permisos antes de cargarse, y --allow-run ejecuta subprocesos fuera del sandbox. Esas son las dos vías por las que el código no confiable escapa.

¿Por qué Deno es seguro por defecto?

Nada se ejecuta con privilegios ambientales: el disco, la red, el entorno y la generación de subprocesos permanecen cerrados hasta que tú los abres. Esa decisión de diseño provino directamente de Ryan Dahl, creador original de Node, que construyó Deno para revertir el comportamiento por defecto de Node de «acceso total a todo». En Deno, las dependencias no obtienen autoridad ambiental propia; en Node, un paquete hereda toda la E/S del sistema que el proceso circundante pueda alcanzar, y esa brecha es la diferencia más marcada entre ambos runtimes.

Node ha añadido desde entonces su propio modelo de permisos. Llegó de forma experimental en Node 20 tras la bandera --experimental-permission, se marcó como estable en la v23.5.0 y Node 24 retiró la variante experimental en favor del simple --permission. El modelo de Deno sigue siendo más profundo: es el comportamiento por defecto en lugar de una bandera opcional, y cubre más clases de capacidades con un alcance más granular.

¿Qué son las banderas de permisos —allow-* de Deno?

Cada capacidad se corresponde con una bandera, y la mayoría admite un argumento con la lista de elementos permitidos. Una bandera sin argumento concede todo lo relativo a esa clase; un argumento la acota. Un --allow-net sin argumento concede acceso a todos los hosts en todos los puertos, mientras que --allow-net=api.example.com:443 restringe el programa exactamente a un host y un puerto.

BanderaProtegeEjemplo acotadoContraparte de denegación
--allow-readLecturas del sistema de archivos--allow-read=./data,config.ini--deny-read
--allow-writeEscrituras en el sistema de archivos--allow-write=./tmp--deny-write
--allow-netAcceso a la red--allow-net=api.example.com:443--deny-net
--allow-envVariables de entorno--allow-env=PORT,HOST--deny-env
--allow-runSubprocesos--allow-run=git,deno--deny-run
--allow-sysAPIs de información del sistema--allow-sys=hostname--deny-sys
--allow-ffiBibliotecas nativas--allow-ffi=./lib.so--deny-ffi
--allow-importImportaciones HTTPS remotas--allow-import=jsr.io--deny-import

Ten en cuenta que no existe --allow-hrtime. Esa bandera fue eliminada en Deno 2.0, y las APIs de temporización de alta resolución como performance.now() están siempre disponibles ahora.

Cuando un script necesita un permiso que no has concedido, Deno se detiene y lo solicita de forma interactiva:

┏ ⚠️ 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) >

Responde y para conceder una vez, n para denegar (lo que lanza Deno.errors.NotCapable) o A para permitir toda la clase. En CI, pasa las banderas de antemano para que nada se bloquee esperando una respuesta.

Cómo creció el conjunto de banderas: precedencia de denegación, --allow-sys, --allow-import y comodines en variables de entorno

Las banderas de denegación llegaron en Deno 1.36 (agosto de 2023), y desde entonces cada bandera --allow-* cuenta con su contraparte --deny-*. Allá donde ambas se solapan, prevalece la denegación, lo que te permite conceder de forma amplia y recortar excepciones:

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

--allow-sys, que se remonta a Deno 1.26 (octubre de 2022), controla las APIs de información del sistema, como Deno.hostname() y Deno.systemMemoryInfo(). La única clase de capacidad genuinamente nueva en Deno 2.0 fue --allow-import, que rige desde qué hosts HTTPS puede tu código descargar módulos en tiempo de ejecución; el HTTP simple nunca está permitido, las importaciones estáticas se filtran automáticamente contra la lista, y especificar tus propios hosts sustituye el conjunto integrado de Deno en lugar de añadirse a él. Usa --deny-import para bloquear hosts concretos de forma tajante.

El acceso al entorno incorporó comodines de sufijo en Deno 2.1. En lugar de enumerar cada variable, acota por prefijo:

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

Declarar permisos en deno.json

Desde Deno 2.5 puedes definir conjuntos de permisos con nombre en deno.json y aplicarlos con -P=nombre (o --permission-set=nombre), manteniendo las banderas de mínimo privilegio bajo control de versiones en lugar de reescribirlas en cada ejecución. Las claves del objeto son los nombres de las banderas (read, write, net, env, sys, run, ffi, import), tal y como se documenta en la referencia de 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"
  }
}

Ejecuta deno run -P=process-data main.ts para el conjunto con nombre, o deno run -P main.ts para el conjunto default. Deno 2.5 también añadió la variable de entorno DENO_AUDIT_PERMISSIONS: apúntala a una ruta de archivo y Deno añadirá una entrada JSONL por cada permiso que el programa toque, se haya concedido o denegado ese acceso. Es una forma rápida de descubrir qué necesita realmente un script.

La API de permisos en tiempo de ejecución

Consulta los permisos desde el código antes de una operación restringida para fallar de forma controlada en lugar de romperse con un error NotCapable. Deno.permissions expone query, request y revoke, y cada uno acepta un descriptor como { 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 informa del estado actual sin solicitar nada, request pregunta al usuario si el estado sigue siendo prompt, y revoke devuelve una capacidad. Esto permite que los programas de larga duración comprueben el estado antes de tocar un recurso y sigan otro camino cuando el acceso no está disponible.

Los dos riesgos: las importaciones y --allow-run

Dos comportamientos permiten que el código no confiable eluda el sandbox, y merece la pena interiorizar ambos. Primero, todo lo que Deno puede resolver estáticamente desde tu punto de entrada (archivos locales, paquetes de npm y JSR, y URLs remotas escritas como literales de cadena) se descarga antes de que el sistema de permisos tenga voz, de modo que una dependencia puede leer su propio código fuente y alcanzar la red antes de que se aplique tu primera bandera --allow-*. Ese pase libre cubre la carga y nada más: en cuanto el código se ejecuta, cada operación vuelve a verificarse. --allow-import acota qué hosts remotos pueden importarse, pero no hace que las importaciones en sí requieran una concesión en tiempo de ejecución, así que audita el código de terceros antes de incorporarlo.

Segundo, --allow-run es el riesgo más afilado: aquello que generes se convierte en un proceso por derecho propio, con los privilegios que le otorgue el sistema operativo y no el conjunto restringido que le pasaste a Deno. Eso significa que --allow-run=deno permite que un script en sandbox relance Deno con --allow-all y escape por completo. Además, solo restringe qué ejecutable se lanza, no sus argumentos: --allow-run=cat permite que el código lea cualquier archivo mediante cat. Acótalo a binarios de confianza concretos, como --allow-run=git, y ten en cuenta que --allow-ffi conlleva la misma clase de riesgo, ya que las bibliotecas nativas se ejecutan como código máquina fuera de las comprobaciones de la capa de JavaScript.

La postura práctica: concede la lista de permisos más estrecha que funcione, añade --deny-* sobre las rutas sensibles y trata --allow-run y --allow-ffi como fronteras de confianza, no como comodidades. Empieza con cero permisos, ejecuta el script y añade exactamente lo que las solicitudes interactivas (o el registro de DENO_AUDIT_PERMISSIONS) te indiquen que necesita.

Preguntas frecuentes

¿Cuál es la diferencia entre responder que no a una solicitud de permiso de Deno y Deno.errors.NotCapable?

Son el mismo resultado desde puntos de entrada distintos. Cuando respondes 'n' a una solicitud interactiva, o ejecutas sin la bandera requerida, la operación denegada lanza Deno.errors.NotCapable en Deno 2 (renombrado desde el antiguo PermissionDenied). El cambio de nombre te permite distinguir las denegaciones de permisos propias de Deno de los errores ordinarios del sistema operativo, como un archivo inexistente, ya que ambos se manifestaban antes como fallos de aspecto similar.

¿--allow-net=example.com permite también HTTPS en el puerto 443?

Sí. Cuando especificas un host sin puerto, como --allow-net=example.com, Deno permite conexiones a ese host en cualquier puerto, incluido el 443. Para restringirlo a un único puerto debes escribirlo explícitamente como --allow-net=example.com:443, lo que entonces bloquea todos los demás puertos de ese host. Un --allow-net sin argumento concede todos los hosts en todos los puertos.

¿Puedo combinar --allow-read con --deny-read en rutas que se solapan?

Sí, y la denegación siempre prevalece. Ejecutar --allow-read=. --deny-read=./secrets concede acceso de lectura a todo el directorio del proyecto excepto a ./secrets, que permanece ilegible. Las banderas de denegación tienen precedencia sobre las de permiso en todas las clases de capacidades, así que este patrón te permite conceder de forma amplia y recortar rutas sensibles en lugar de enumerar individualmente cada archivo permitido.

¿Necesito --allow-import para usar paquetes de npm o JSR?

No, no para paquetes importados estáticamente. Todo lo que Deno puede resolver desde tu punto de entrada sin ejecutar código, incluidos los paquetes de npm y JSR, se descarga antes de consultar el sistema de permisos. --allow-import decide de qué hosts HTTPS pueden proceder las importaciones remotas, y el HTTP simple nunca es una opción. Un especificador calculado en tiempo de ejecución es distinto: una URL remota dinámica necesita --allow-import, y una ruta local dinámica necesita --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.