Deno Permission System: Architecture Note
An architecture note covering Deno’s permission system: requirements, minimal design, trust boundaries, operational checks, failure modes, and when the design would change.
17 Aug 2025, 14:10 UTC

Requirements
When executing third‑party scripts, Deno must prevent unintended side‑effects such as reading arbitrary files, opening network connections, or inspecting environment variables. The core requirement is that every privileged operation be gated by an explicit, user‑granted permission.
Smallest Suitable Design
Deno implements a lightweight permission table that lives inside the runtime process. Each privileged API (e.g., Deno.readFile, Deno.connect, Deno.env.get) maps to a permission descriptor (--allow-read, --allow-net, --allow-env). Before the call proceeds, the runtime checks the table; if the flag is missing it throws a PermissionDenied error.
Trust and Data Boundaries
The Deno runtime itself is the trusted computing base (TCB). User‑provided JavaScript/TypeScript code runs in an untrusted sandbox. The only way for code to cross the sandbox boundary to the host OS is through the permission‑checked APIs. All other interactions (pure computation, internal objects) remain inside the sandbox.
Operational Checks
At each privileged call site the runtime performs:
- Lookup of the required permission flag in the process‑wide permission table.
- If present, the call is forwarded to the underlying OS resource.
- If absent, a
PermissionDeniedexception is thrown, which can be caught or will abort the script.
These checks are synchronous and add negligible overhead because they are simple set‑membership tests.
Failure Modes
- Native bypass: If a script gains access to a native addon or uses FFI (
Deno.dlopen) without the--allow-ffiflag, the runtime will throwPermissionDenied. However, a malicious native module that is loaded via other means (e.g., pre‑linked binary) could break the sandbox. - Privileged built‑in modules: Should Deno ship a native module that performs privileged operations without consulting the permission table, the trust boundary would shift. The design would then need either to remove that module or to wrap its entry points with permission checks.
- Resource exhaustion: The permission system does not limit CPU or memory. A script could still launch a denial‑of‑service attack by spinning tight loops or allocating large arrays.
Conditions That Would Change the Design
- Introduction of a new privileged capability (e.g., GPU access) that is not covered by existing flags would require adding a new permission descriptor and updating the table lookup logic.
- Adoption of a capability‑based security model where permissions are granted to specific objects rather than globally would necessitate a richer permission representation (e.g., capability tokens) and a more complex check.
- If Deno were to embed a just‑in‑time compiled native sandbox that could execute arbitrary machine code, the current API‑level checks would be insufficient, prompting a redesign toward process‑level isolation.
Practical Verification
To confirm that the permission checks are active, run the following commands in a terminal where deno is available.
1. Missing read permission
# Create a test script that attempts to read /etc/passwd
cat > permission_test.ts <<'EOF'
try {
const data = await Deno.readTextFile('/etc/passwd');
console.log('File read, first line:', data.split('\n')[0]);
} catch (e) {
if (e.name === 'PermissionDenied') {
console.error('Expected error: read permission missing');
} else {
throw e;
}
}
EOF
# Run without --allow-read (should fail)
deno run permission_test.ts
Expected output: the script prints an error message about missing read permission and exits with a non‑zero status.
2. Granting read permission
# Same script, now with the flag
deno run --allow-read permission_test.ts
Expected output: the first line of /etc/passwd is printed, showing the call succeeded.
3. Network permission check
cat > net_test.ts <<'EOF'
try {
const conn = await Deno.connect({ hostname: 'example.com', port: 80 });
console.log('Connected to', conn.remoteAddr);
conn.close();
} catch (e) {
if (e.name === 'PermissionDenied') {
console.error('Expected error: network permission missing');
} else {
throw e;
}
}
EOF
# Run without --allow-net
deno run net_test.ts
# Run with --allow-net
deno run --allow-net net_test.ts
Similar behavior: missing flag yields PermissionDenied; with flag the connection succeeds.
4. FFI permission check
cat > ffi_test.ts <<'EOF'
try {
const lib = Deno.dlopen('libc.so.6', { 'puts': { parameters: ['pointer'], result: 'number' } });
console.error('FFI should have been denied');
} catch (e) {
if (e.name === 'PermissionDenied') {
console.error('Expected error: FFI permission missing');
} else {
throw e;
}
}
EOF
# Run without --allow-ffi
deno run ffi_test.ts
# Run with --allow-ffi (requires a suitable library on the system)
# deno run --allow-ffi ffi_test.ts
Again, the missing flag triggers the expected error.
Limitations and Complementary Measures
The permission system protects against accidental or inadvertent access but does not stop a determined attacker from exhausting CPU, memory, or file descriptors. For truly untrusted workloads, consider running Deno inside an OS‑level sandbox (e.g., Linux cgroups, namespaces, or a container) and setting resource limits there.
In headless CI environments the interactive prompt is disabled; you must supply all needed flags on the command line, otherwise the script will hang waiting for user input. Automated verification (as shown above) ensures the flags are correctly applied.
Summary
Deno’s permission model satisfies the requirement of explicit user consent via a minimal runtime table, clear trust boundaries, and straightforward operational checks. Its failure modes are well understood, and the design would only need revision if new privileged capabilities are added, if the model shifts to fine‑grained capabilities, or if the runtime itself begins to execute untrusted native code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.