-
Notifications
You must be signed in to change notification settings - Fork 4k
Secure Mode: Deno & Node.js compatible permissions for Bun, but better #25911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
06b9f92
0895fb9
85183b1
48443f0
2a5de8d
edc718c
0880d9a
5864981
674c5de
4c6fb44
de61258
41375a6
312dd9b
07c4a3a
686bdf5
a938175
532f5dd
3c3daa7
18f81c1
ec038d8
cee3810
9f53ad6
3758594
b454054
663b7d3
8d06433
944b323
6c8f994
f496ca9
040c510
68c5da7
ac6beec
73acb41
1d6a77f
4877e14
a402594
1ccf39d
101f401
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,3 +193,4 @@ scripts/lldb-inline | |
|
|
||
| # We regenerate these in all the build scripts | ||
| cmake/sources/*.txt | ||
| bun_secure | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,335 @@ | ||
| --- | ||
| title: Permissions | ||
| description: Control what resources your code can access with Bun's permissions model | ||
| --- | ||
|
|
||
| Bun includes a permissions model that lets you control what resources your code can access. This is useful for running untrusted code or hardening production applications. | ||
|
|
||
| --- | ||
|
|
||
| ## Security modes | ||
|
|
||
| By default, Bun allows all operations for backwards compatibility. Use `--secure` to enable the security sandbox: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Default mode: everything is allowed | ||
| bun script.js | ||
|
|
||
| # Secure mode: may prompt for permissions interactively | ||
| bun --secure script.js | ||
|
|
||
| # Secure mode without prompts: always deny unless explicitly allowed | ||
| bun --secure --no-prompt script.js | ||
| ``` | ||
|
|
||
| In secure mode, operations like file I/O, network access, and subprocess spawning require permission. By default, the user may be prompted interactively. With `--no-prompt`, operations throw an error unless explicitly granted. | ||
|
|
||
| When permission is denied, the error message includes details about what's needed: | ||
|
|
||
| ``` | ||
| PermissionDenied: Requires read access to "/path/to/file", run with --allow-read | ||
| ``` | ||
|
|
||
| <Note> | ||
| The `--allow-*` and `--deny-*` flags only take effect when `--secure` is specified. Without `--secure`, all operations are allowed regardless of permission flags. | ||
| </Note> | ||
|
|
||
| --- | ||
|
|
||
| ## Granting permissions | ||
|
|
||
| Use `--allow-*` flags to grant specific permissions: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow reading files | ||
| bun --secure --allow-read script.js | ||
|
|
||
| # Allow network access | ||
| bun --secure --allow-net script.js | ||
|
|
||
| # Allow multiple permission types | ||
| bun --secure --allow-read --allow-net --allow-env script.js | ||
| ``` | ||
|
|
||
| To grant all permissions at once, use `-A` or `--allow-all`: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| bun --secure -A script.js | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Permission types | ||
|
|
||
| | Permission | Flag | Description | | ||
| |------------|------|-------------| | ||
| | File Read | `--allow-read` | Read files and directories | | ||
| | File Write | `--allow-write` | Write, create, or delete files | | ||
| | Network | `--allow-net` | Make network requests and listen on ports | | ||
| | Environment | `--allow-env` | Access environment variables | | ||
| | Subprocess | `--allow-run` | Spawn child processes | | ||
| | FFI | `--allow-ffi` | Load native libraries | | ||
| | System Info | `--allow-sys` | Access system information | | ||
|
|
||
| --- | ||
|
|
||
| ## Granular permissions | ||
|
|
||
| Each permission can be scoped to specific resources. | ||
|
|
||
| ### File system | ||
|
|
||
| Restrict file access to specific paths: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow reading only from ./src | ||
| bun --secure --allow-read=./src script.js | ||
|
|
||
| # Allow reading from multiple paths | ||
| bun --secure --allow-read=./src,./config,/tmp script.js | ||
|
|
||
| # Allow writing only to ./dist | ||
| bun --secure --allow-read --allow-write=./dist build.js | ||
| ``` | ||
|
|
||
| Path matching notes: | ||
| - Directory prefix matching: `--allow-read=/tmp` allows access to `/tmp/foo/bar` | ||
| - Basename matching: `--deny-read=.env` blocks any file named `.env` in any directory | ||
| - Both `/` and `\` separators are supported on Windows | ||
|
|
||
| ### Network | ||
|
|
||
| Restrict network access to specific hosts: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow only api.example.com | ||
| bun --secure --allow-net=api.example.com script.js | ||
|
|
||
| # Allow localhost on any port | ||
| bun --secure --allow-net=localhost script.js | ||
|
|
||
| # Allow specific port | ||
| bun --secure --allow-net=localhost:3000 script.js | ||
|
|
||
| # Allow port range | ||
| bun --secure --allow-net=localhost:3000-4000 script.js | ||
|
|
||
| # Allow IPv6 localhost | ||
| bun --secure --allow-net=[::1]:3000 script.js | ||
|
|
||
| # Allow IPv6 address | ||
| bun --secure --allow-net=[2001:db8::1]:443 script.js | ||
| ``` | ||
|
|
||
| <Note> | ||
| IPv6 addresses must be enclosed in square brackets when specifying a port, e.g., `[::1]:3000`. | ||
| </Note> | ||
|
|
||
| #### Network wildcards | ||
|
|
||
| Use wildcards to match multiple hosts: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Single-segment wildcard: matches one subdomain level | ||
| bun --secure --allow-net="*.example.com" script.js | ||
| # Matches: api.example.com, www.example.com | ||
| # Does NOT match: api.v2.example.com | ||
|
|
||
| # Multi-segment wildcard: matches multiple subdomain levels | ||
| bun --secure --allow-net="**.example.com" script.js | ||
| # Matches: api.example.com, api.v2.example.com, a.b.c.example.com | ||
| ``` | ||
|
|
||
kynnyhsap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ### Environment variables | ||
|
|
||
| Restrict access to specific variables: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow only DATABASE_URL | ||
| bun --secure --allow-env=DATABASE_URL script.js | ||
|
|
||
| # Allow multiple variables | ||
| bun --secure --allow-env=DATABASE_URL,API_KEY,NODE_ENV script.js | ||
|
|
||
| # Allow variables matching a prefix | ||
| bun --secure --allow-env=AWS_* script.js | ||
| ``` | ||
|
|
||
| ### Subprocesses | ||
|
|
||
| Restrict which commands can be spawned: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow only git | ||
| bun --secure --allow-run=git script.js | ||
|
|
||
| # Allow multiple commands | ||
| bun --secure --allow-run=git,npm,node script.js | ||
| ``` | ||
|
|
||
kynnyhsap marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| --- | ||
|
|
||
| ## Denying permissions | ||
|
|
||
| Use `--deny-*` flags to explicitly block access, even when a broader permission is granted: | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Allow reading everything except .env files | ||
| bun --secure --allow-read --deny-read=.env script.js | ||
|
|
||
| # Allow network except internal hosts | ||
| bun --secure --allow-net --deny-net=*.internal.corp script.js | ||
| ``` | ||
|
|
||
| Deny rules take precedence over allow rules. | ||
|
|
||
| --- | ||
|
|
||
| ## JavaScript API | ||
|
|
||
| Query and manage permissions at runtime using `Bun.permissions`: | ||
|
|
||
| ```ts | ||
| // Check permission state | ||
| const status = Bun.permissions.querySync({ name: "read", path: "/tmp" }); | ||
| console.log(status.state); // "granted" | "denied" | "prompt" | ||
|
|
||
| // Async query | ||
| const netStatus = await Bun.permissions.query({ | ||
| name: "net", | ||
| host: "example.com" | ||
| }); | ||
|
|
||
| // Request permission (returns current state, may prompt user) | ||
| const requested = await Bun.permissions.request({ name: "env" }); | ||
|
|
||
| // Revoke permission (denies the entire permission type) | ||
| const revoked = await Bun.permissions.revoke({ name: "read" }); | ||
| ``` | ||
|
|
||
| <Note> | ||
| `revoke()` denies the entire permission type, not just a specific resource. Calling `revoke({ name: "read", path: "/tmp" })` will deny all read access, not just to `/tmp`. | ||
| </Note> | ||
|
|
||
| ### Permission states | ||
|
|
||
| - **`granted`**: The operation is allowed | ||
| - **`denied`**: The operation is blocked | ||
| - **`prompt`**: The user may be prompted for permission (only when running with `--secure` and without `--no-prompt`) | ||
|
|
||
| <Note> | ||
| The `prompt` state only exists when `--secure` is used without `--no-prompt`. In other modes, permissions are either `granted` (default mode) or `denied` (secure mode with `--no-prompt`). | ||
| </Note> | ||
|
|
||
| ### request() behavior | ||
|
|
||
| `request()` returns the current permission state. If the state is `prompt`, the user may be prompted interactively. If permissions are already `granted` or `denied`, no prompt is shown and the current state is returned. | ||
|
|
||
| ### Permission descriptors | ||
|
|
||
| ```ts | ||
| // File system | ||
| { name: "read", path: "/path/to/file" } | ||
| { name: "write", path: "/path/to/dir" } | ||
|
|
||
| // Network | ||
| { name: "net", host: "example.com" } | ||
| { name: "net", host: "example.com:443" } | ||
|
|
||
| // Environment | ||
| { name: "env", variable: "DATABASE_URL" } | ||
|
|
||
| // Subprocess | ||
| { name: "run", command: "git" } | ||
|
|
||
| // System info | ||
| { name: "sys", kind: "hostname" } | ||
|
|
||
| // FFI (CLI supports --allow-ffi=<path>, but JS API is unscoped) | ||
| { name: "ffi" } | ||
| ``` | ||
|
Comment on lines
+247
to
+250
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for FFI permission handling in the codebase
rg -n "ffi.*permission|permission.*ffi" --type=zig -C 3 src/bun.js/Repository: oven-sh/bun Length of output: 37 🏁 Script executed: #!/bin/bash
# Also search for FFI permission query handling more broadly
rg -n "\"ffi\"" --type=zig src/ -C 2 | grep -i "permission\|query\|scope" | head -30Repository: oven-sh/bun Length of output: 128 🏁 Script executed: #!/bin/bash
# Search for permission query implementations
rg -n "queryPermission\|hasPermission" --type=zig -C 2 src/ | grep -A 5 -B 5 "ffi" | head -40Repository: oven-sh/bun Length of output: 37 🏁 Script executed: #!/bin/bash
# Look for CLI flag definitions for --allow-ffi
rg -n "allow-ffi|allow_ffi" --type=zig src/ -C 3 | head -50Repository: oven-sh/bun Length of output: 3570 🏁 Script executed: #!/bin/bash
# Search for permission query methods in JS bindings
rg -n "queryPermission|hasPermission|checkPermission" --type=zig src/bun.js/ -C 3Repository: oven-sh/bun Length of output: 37 🏁 Script executed: #!/bin/bash
# Search for how FFI permissions are checked/queried in JS API
rg -n "permission.*query|query.*permission" --type=zig src/bun.js/ -C 3Repository: oven-sh/bun Length of output: 1350 🏁 Script executed: #!/bin/bash
# Look for permission descriptor handling
rg -n "PermissionDescriptor|permission.*descriptor" --type=zig src/bun.js/ -C 3Repository: oven-sh/bun Length of output: 1811 🏁 Script executed: #!/bin/bash
# Search for FFI permission-related code in JS API
fd ".*\.zig$" src/bun.js/ | xargs rg -l "ffi" | head -10Repository: oven-sh/bun Length of output: 393 🏁 Script executed: #!/bin/bash
# Read the parseDescriptor function implementation
sed -n '1475,1540p' src/bun.js/api/BunObject.zigRepository: oven-sh/bun Length of output: 3343 🏁 Script executed: #!/bin/bash
# Also check what ParsedDescriptor struct contains
rg -n "struct ParsedDescriptor|ParsedDescriptor.*=" src/bun.js/api/BunObject.zig -A 10 -B 2Repository: oven-sh/bun Length of output: 439 🏁 Script executed: #!/bin/bash
# Check how permission queries handle FFI specifically
sed -n '1530,1575p' src/bun.js/api/BunObject.zigRepository: oven-sh/bun Length of output: 2173 🏁 Script executed: #!/bin/bash
# Check the permissions.check function implementation
rg -n "fn check\|pub fn check" src/permissions.zig -A 20 | head -50Repository: oven-sh/bun Length of output: 37 🏁 Script executed: #!/bin/bash
# Look for any FFI-specific restrictions in permission checking
rg -n "\.ffi" src/permissions.zig -B 3 -A 3Repository: oven-sh/bun Length of output: 1060 Update FFI permission documentation to reflect actual JS API capabilities. The documentation incorrectly states the JS API only supports unscoped FFI queries ( 🤖 Prompt for AI Agents |
||
|
|
||
| <Note> | ||
| FFI permissions can be scoped to specific library paths via CLI (`--allow-ffi=/path/to/lib.so`), but the JavaScript API only supports unscoped FFI queries. | ||
| </Note> | ||
|
|
||
| --- | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Web server | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| bun --secure \ | ||
| --allow-read=./public,./views \ | ||
| --allow-net=localhost:3000 \ | ||
| --allow-env=PORT,NODE_ENV \ | ||
| server.js | ||
| ``` | ||
|
|
||
| ### API client | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| bun --secure \ | ||
| --allow-net="https://*.api.example.com" \ | ||
| --allow-env=API_KEY \ | ||
| client.js | ||
| ``` | ||
|
|
||
| ### Build script | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| bun --secure \ | ||
| --allow-read=./src \ | ||
| --allow-write=./dist \ | ||
| --allow-run=esbuild,tsc \ | ||
| --allow-env=NODE_ENV \ | ||
| build.js | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Workers | ||
|
|
||
| Workers inherit permissions from their parent. If the main script has `--allow-read`, workers spawned from it also have read access. | ||
|
|
||
| ```ts | ||
| // main.ts - run with: bun --secure --allow-read main.ts | ||
| const worker = new Worker(new URL("./worker.ts", import.meta.url)); | ||
| // worker.ts automatically has --allow-read permission | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Security model | ||
|
|
||
| Bun's permissions follow a fail-closed design: | ||
|
|
||
| - **Deny by default**: In `--secure` mode, all sensitive operations are denied unless explicitly allowed | ||
| - **Deny takes precedence**: `--deny-*` rules override `--allow-*` rules | ||
| - **Path matching**: `/tmp` allows access to `/tmp/foo/bar` (directory prefix matching) | ||
| - **Invalid patterns fail closed**: Malformed permission patterns result in denied access | ||
|
|
||
| <Note> | ||
| Without the `--secure` flag, Bun runs in permissive mode for backwards compatibility. All operations are allowed by default. | ||
| </Note> | ||
|
|
||
| ### Symlink handling | ||
|
|
||
| In `--secure` mode, Bun resolves symbolic links to their real paths before checking permissions. This prevents symlink-based attacks where a symlink in an allowed directory points to a forbidden location. | ||
|
|
||
| ```bash terminal icon="terminal" | ||
| # Example directory structure: | ||
| # /app/allowed/link -> /etc/passwd (symlink) | ||
|
|
||
| # This will be DENIED because the symlink target is outside allowed paths | ||
| bun --secure --allow-read=/app/allowed script.js | ||
| # script.js tries to read /app/allowed/link | ||
| # Bun resolves it to /etc/passwd and denies access | ||
| ``` | ||
|
|
||
| Symlink resolution applies to all file system operations in secure mode, including nested symlink chains. The entire chain is resolved to find the final target path, which is then checked against permissions. | ||
|
|
||
| <Note> | ||
| Symlink resolution only occurs in `--secure` mode. In default (permissive) mode, symlinks are not resolved before access, maintaining full backwards compatibility with no performance overhead. | ||
| </Note> | ||
Uh oh!
There was an error while loading. Please reload this page.