On this page
Security and permissions
Deno is secure by default. Unless you specifically enable it, a program run with Deno has no access to sensitive APIs, such as file system access, network connectivity, or environment access. You must explicitly grant access to these resources with command line flags or with a runtime permission prompt. This is a major difference from Node, where dependencies are automatically granted full access to all system I/O, potentially introducing hidden vulnerabilities into your project.
Before using Deno to run completely untrusted code, read the section on executing untrusted code below.
Key principles Jump to heading
Before diving into the specifics of permissions, it's important to understand the key principles of Deno's security model:
- No access to I/O by default: Code executing in a Deno runtime has no access to read or write arbitrary files on the file system, to make network requests or open network listeners, to access environment variables, or to spawn subprocesses.
- No limits on the execution of code at the same privilege level: Deno
allows the execution of any code (JS/TS/Wasm) via multiple means, including
eval,new Function, dynamic imports and web workers at the same privilege level with little restriction as to where the code originates (network, npm, JSR, etc). - Multiple invocations of the same application can share data: Deno provides a mechanism for multiple invocations of the same application to share data, through built in caching and KV storage APIs. Different applications can not see each other's data.
- All code executing on the same thread shares the same privilege level: All code executing on the same thread shares the same privilege level. It is not possible for different modules to have different privilege levels within the same thread.
- Code can not escalate its privileges without user consent: Code executing in a Deno runtime can not escalate its privileges without the user agreeing explicitly to an escalation via interactive prompt or a invocation time flag.
- The initial static module graph can import modules without restrictions:
All modules that are imported in the initial static module graph (local files,
npm packages, jsr packages, and remote URLs) are loaded by the runtime without
consulting the permission system. No
--allow-readis required to load local files, and no--allow-netis required to fetch remote modules. The static graph includes staticimportstatements andimport()calls whose specifier is a string literal: anything that can be resolved without running code. This exemption applies only to loading. Once code runs, anything it does still goes through the permission system, andimport()calls with non-literal specifiers (e.g.import(someVariable)) are checked against--allow-read/--allow-importat runtime.
These key principles are designed to provide an environment where a user can execute code with minimal risk of harm to the host machine or network. The security model is designed to be simple to understand and to provide a clear separation of concerns between the runtime and the code executing within it. The security model is enforced by the Deno runtime, and is not dependent on the underlying operating system.
Permissions Jump to heading
Deno is sandboxed by default: code cannot touch the file system, network,
environment, or run subprocesses unless you allow it. Permissions are not all or
nothing. You grant them with --allow-* flags, and most can be scoped to
specific resources, so a program gets exactly the access it needs and no more.
Run a program with no flags and a sensitive operation is refused:
deno run main.ts
# error: Requires net access to "example.com", run again with the --allow-net flag
Grant access scoped to just the host the program should reach:
deno run --allow-net=example.com main.ts
The same scoping works for the other permissions: --allow-read=./data limits
reads to one directory, --allow-env=API_KEY to a single variable, and so on. A
bare --allow-net with no value grants the whole category, which is broader
than most programs need.
When an operation is refused (because the permission was not granted or was
explicitly denied), Deno throws a NotCapable error. You can catch it like any
other error if you want to handle missing permissions gracefully:
try {
await Deno.readTextFile("/etc/hosts");
} catch (err) {
if (err instanceof Deno.errors.NotCapable) {
console.error("Missing read permission, run again with --allow-read");
}
}
When stdout is a terminal and you have not passed a flag, Deno pauses and asks instead of failing, so you can grant access interactively as it is requested:
⚠️ Deno requests net access to "example.com". Run again with --allow-net to bypass this prompt.
Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
--deny-* flags override their --allow-* counterparts, so you can grant a
broad category and carve out the sensitive parts:
--allow-read --deny-read=/etc reads anything except /etc. At the other
extreme, -A (--allow-all) grants everything and turns the sandbox off
entirely, giving the same access as running code in Node, so reach for it
sparingly.
See the Permissions reference for every
permission and its flags, and
permissions in deno.json for
declaring them in your configuration file.
Permissions that bypass the sandbox Jump to heading
A few permissions grant access to things Deno cannot sandbox, so granting them is effectively granting full access:
--allow-runlets the program spawn subprocesses. A subprocess runs as a separate program with its own permissions, not the restricted set you granted the Deno process, so whatever it does happens outside the sandbox. This is why--allow-run=denois especially dangerous: a script that can start a newdenoprocess can start it with--allow-all, inheriting none of the parent's limits and escaping the sandbox entirely. Grant--allow-runonly for specific, trusted executables where you can (--allow-run=git), and avoid giving a sandboxed program the ability to launchdenoor a shell.--allow-ffiloads native libraries through FFI or Node-API addons. Deno enforces permissions in the JavaScript layer, but a native library runs as compiled machine code in the same process and can issue system calls directly. Once loaded, it can read files, open sockets, or do anything the operating system lets the process do, regardless of which--allow-*flags you passed.
Be careful when combining --allow-write with --allow-run. Write access to a
directory that contains an allowed executable (or to the executable itself) lets
a program overwrite that binary, so the next subprocess it spawns runs
attacker-controlled code with the host's privileges. Avoid granting write access
to directories that hold binaries you allow with --allow-run.
Treat both as equivalent to --allow-all when deciding whether to trust the
code you are running.
Adjusting permissions at runtime Jump to heading
A program can inspect and tighten its own permissions through the
Deno.permissions API.
Deno.permissions.query reports whether a
permission is granted,
Deno.permissions.request prompts for one on
demand, and Deno.permissions.revoke
downgrades a granted permission back to the prompt state:
// Read a config file at startup, then give up read access.
const config = JSON.parse(await Deno.readTextFile("./config.json"));
await Deno.permissions.revoke({ name: "read" });
// Later reads now have to prompt again, and fail outright under --no-prompt.
Dropping permissions you no longer need after startup is a simple way to shrink the attack surface available to the rest of your code, including your dependencies.
Evaluation of code Jump to heading
Deno sets no limits on the execution of code at the same privilege level. This
means that code executing in a Deno runtime can use eval, new Function, or
even dynamic import or web workers to execute arbitrary code with the same
privilege level as the code that called eval, new Function, or the dynamic
import or web worker.
This code can be hosted on the network, be in a local file (if read permissions
are granted), or be stored as plain text in a string inside of the code that
called eval, new Function, or the dynamic import or web worker.
Executing untrusted code Jump to heading
While Deno provides security features that are designed to protect the host machine and network from harm, untrusted code is still scary. When executing untrusted code, it is important to have more than one layer of defense. Some suggestions for executing untrusted code are outlined below, and we recommend using all of these when executing arbitrary untrusted code:
- Run
denowith limited permissions and determine upfront what code actually needs to run (and prevent more code being loaded using--frozenlockfile and--cached-only). - Isolate the untrusted part in a Web Worker with a reduced permission set, so it cannot inherit everything the main program was granted.
- Use OS provided sandboxing mechanisms like
chroot,cgroups,seccomp, etc. - Use a sandboxed environment like a VM or MicroVM (gVisor, Firecracker, etc).
Auditing dependencies Jump to heading
The permission sandbox controls what code can do at runtime, but it does not
tell you whether your dependencies contain known vulnerabilities. Deno ships
deno audit to scan your dependencies against
vulnerability databases, which is useful as a CI gate. See
supply chain management for keeping
dependencies safe over time.
Permission broker Jump to heading
Using a permission broker changes Deno's decision authority: CLI flags and
prompts no longer apply. Ensure your broker process is resilient, audited, and
available before enabling DENO_PERMISSION_BROKER_PATH.
For centralized and policy-driven permission decisions, Deno can delegate all
permission checks to an external broker process. Enable this by setting the
DENO_PERMISSION_BROKER_PATH environment variable to a path that Deno will use
to connect to the broker:
- On Unix-like systems: a Unix domain socket path (for example,
/tmp/deno-perm.sock). - On Windows: a named pipe (for example,
\\.\pipe\deno-perm-broker).
When a permission broker is active:
- All
--allow-*and--deny-*flags are ignored. - Interactive permission prompts are not shown (equivalent to non-interactive mode).
- Every permission check is sent to the broker; the broker must reply with a decision for each request.
If anything goes wrong during brokering (for example: Deno cannot connect to the socket/pipe, messages are malformed, arrive out of order, IDs do not match, or the connection closes unexpectedly), Deno immediately terminates the process to preserve integrity and prevent permission escalation.
The request/response message shapes are versioned and defined by JSON Schemas:
- Request schema: permission-broker-request.v1.json
- Response schema: permission-broker-response.v1.json
Each request contains a version (v), the Deno process ID (pid), a unique
monotonic request id, a timestamp (datetime, RFC 3339), the permission
name, and an optional value depending on permission type. The response must
echo the id and include a result of either "allow" or "deny". When
denied, a human-readable reason may be included.
Example message flow:
-> req {"v":1,"pid":10234,"id":1,"datetime":"2025-01-01T00:00:00.000Z","permission":"read","value":"./run/permission_broker/scratch.txt"}
<- res {"id":1,"result":"allow"}
-> req {"v":1,"pid":10234,"id":2,"datetime":"2025-01-01T00:00:01.000Z","permission":"read","value":"./run/permission_broker/scratch.txt"}
<- res {"id":2,"result":"allow"}
-> req {"v":1,"pid":10234,"id":3,"datetime":"2025-01-01T00:00:02.000Z","permission":"read","value":"./run/permission_broker/log.txt"}
<- res {"id":3,"result":"allow"}
-> req {"v":1,"pid":10234,"id":4,"datetime":"2025-01-01T00:00:03.000Z","permission":"write","value":"./run/permission_broker/log.txt"}
<- res {"id":4,"result":"allow"}
-> req {"v":1,"pid":10234,"id":5,"datetime":"2025-01-01T00:00:04.000Z","permission":"env","value":null}
<- res {"id":5,"result":"deny","reason":"Environment access is denied."}