Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
06b9f92
feat: implement Deno-compatible security/permissions model
kynnyhsap Jan 8, 2026
0895fb9
fix: complete permissions model implementation and tests
kynnyhsap Jan 8, 2026
85183b1
feat: add permission checks to os.freemem/totalmem and comprehensive …
kynnyhsap Jan 8, 2026
48443f0
fix: address PR review comments for permissions model
kynnyhsap Jan 8, 2026
2a5de8d
test: skip ls command test on Windows
kynnyhsap Jan 9, 2026
edc718c
feat(permissions): implement file system permission checks
kynnyhsap Jan 9, 2026
0880d9a
feat(permissions): add network wildcard pattern support
kynnyhsap Jan 9, 2026
5864981
test(permissions): expand network wildcard test coverage
kynnyhsap Jan 9, 2026
674c5de
fix(permissions): address PR review comments
kynnyhsap Jan 9, 2026
4c6fb44
fix(permissions): fail closed on invalid port patterns
kynnyhsap Jan 9, 2026
de61258
fix(permissions): handle path edge cases for Windows and trailing sep…
kynnyhsap Jan 9, 2026
41375a6
fix(permissions): check abort signal before permissions, add stderr a…
kynnyhsap Jan 9, 2026
312dd9b
test(permissions): enable --deny-read test
kynnyhsap Jan 9, 2026
07c4a3a
chore(permissions): remove unused imports and fields
kynnyhsap Jan 9, 2026
686bdf5
Add FFI/Worker permission tests and documentation
kynnyhsap Jan 9, 2026
a938175
docs: clarify revoke() behavior and add request/revoke examples
kynnyhsap Jan 9, 2026
532f5dd
Address CodeRabbit review feedback
kynnyhsap Jan 9, 2026
3c3daa7
docs: address CodeRabbit review feedback on permissions
kynnyhsap Jan 9, 2026
18f81c1
test: add failing tests for Bun.file() permission checks
kynnyhsap Jan 9, 2026
ec038d8
fix: add permission checks for Bun.file() and Bun.write()
kynnyhsap Jan 9, 2026
cee3810
Address CodeRabbit review feedback on permissions
kynnyhsap Jan 10, 2026
9f53ad6
Use relative paths for test.ts in spawn commands
kynnyhsap Jan 10, 2026
3758594
Add permission system performance benchmarks
kynnyhsap Jan 10, 2026
b454054
Split benchmark into sync/async to avoid await overhead
kynnyhsap Jan 10, 2026
663b7d3
Address CodeRabbit review: make tests cross-platform and non-interactive
kynnyhsap Jan 12, 2026
8d06433
Add bunfig.toml support for permission options
kynnyhsap Jan 12, 2026
944b323
Address CodeRabbit review: refactor --allow-* parsing and improve tests
kynnyhsap Jan 12, 2026
6c8f994
Address CodeRabbit review: use cross-platform env vars in tests
kynnyhsap Jan 12, 2026
f496ca9
Add bun_secure to .gitignore
kynnyhsap Jan 12, 2026
040c510
Address CodeRabbit review: add explicit exitCode assertions
kynnyhsap Jan 12, 2026
68c5da7
Implement permission checks for fs.open and fs.statfs operations
kynnyhsap Jan 12, 2026
ac6beec
Address CodeRabbit review: bundler checks, symlink docs, memory docs
kynnyhsap Jan 12, 2026
73acb41
feat: add Node.js permission model compatibility
kynnyhsap Jan 12, 2026
1d6a77f
refactor: improve permission code quality
kynnyhsap Jan 12, 2026
4877e14
docs: clarify prompt state handling in permission check
kynnyhsap Jan 12, 2026
a402594
feat: add symlink resolution for path permissions in secure mode
kynnyhsap Jan 12, 2026
1ccf39d
docs: add symlink resolution behavior to permissions documentation
kynnyhsap Jan 12, 2026
101f401
refactor: simplify permissions.zig code
kynnyhsap Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ scripts/lldb-inline

# We regenerate these in all the build scripts
cmake/sources/*.txt
bun_secure
335 changes: 335 additions & 0 deletions docs/runtime/permissions.mdx
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
```

### 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
```

---

## 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -30

Repository: 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 -40

Repository: 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 -50

Repository: 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 3

Repository: 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 3

Repository: 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 3

Repository: 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 -10

Repository: 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.zig

Repository: 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 2

Repository: 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.zig

Repository: 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 -50

Repository: 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 3

Repository: 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 ({ name: "ffi" }). The implementation actually supports scoped queries with a path parameter: { name: "ffi", path: "/some/path" }. The JS API and CLI both support path-scoped FFI permissions. Update the comment and example to clarify that the JS API supports scoped FFI queries.

🤖 Prompt for AI Agents
In @docs/runtime/permissions.mdx around lines 247 - 250, Update the FFI
permission snippet and surrounding comment to show that the JS API supports
scoped FFI queries by adding the path parameter; replace or extend the example
`{ name: "ffi" }` with `{ name: "ffi", path: "/some/path" }` (and note that the
CLI also supports `--allow-ffi=<path>`), so the docs and example correctly
reflect that both JS API and CLI accept path-scoped FFI permissions.


<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>
4 changes: 2 additions & 2 deletions src/bake/DevServer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3663,7 +3663,7 @@ pub fn writeMemoryVisualizerMessage(dev: *DevServer, payload: *std.array_list.Ma
system_total: u32,
};
const cost = dev.memoryCostDetailed();
const system_total = bun.api.node.os.totalmem();
const system_total = bun.api.node.os.totalmemImpl();
try w.writeStruct(Fields{
.incremental_graph_client = @truncate(cost.incremental_graph_client),
.incremental_graph_server = @truncate(cost.incremental_graph_server),
Expand All @@ -3676,7 +3676,7 @@ pub fn writeMemoryVisualizerMessage(dev: *DevServer, payload: *std.array_list.Ma
else
0,
.process_used = @truncate(bun.sys.selfProcessMemoryUsage() orelse 0),
.system_used = @truncate(system_total -| bun.api.node.os.freemem()),
.system_used = @truncate(system_total -| bun.api.node.os.freememImpl()),
.system_total = @truncate(system_total),
});

Expand Down
2 changes: 2 additions & 0 deletions src/bun.js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub const Run = struct {
.smol = ctx.runtime_options.smol,
.debugger = ctx.runtime_options.debugger,
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
.permission_options = &ctx.runtime_options.permissions,
}),
.arena = arena,
.ctx = ctx,
Expand Down Expand Up @@ -189,6 +190,7 @@ pub const Run = struct {
.debugger = ctx.runtime_options.debugger,
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
.is_main_thread = true,
.permission_options = &ctx.runtime_options.permissions,
},
),
.arena = arena,
Expand Down
Loading