Security
Overview of the security protections applied by the Buntime runtime: CSRF, request ID, reserved paths, path validation, sensitive env var filtering in workers, secure auto-install, body/header limits, and recommended deploy practices.
For /data directories, env vars, and manifest validation at startup, see Environments. For log correlation with X-Request-Id, see Logging.
CSRF protection
Section titled “CSRF protection”The runtime enforces CSRF (Cross-Site Request Forgery) validation on state-mutating methods.
Protected methods
Section titled “Protected methods”POST, PUT, PATCH, DELETE.
Validation rules
Section titled “Validation rules”- Origin required — protected methods must include an
Originheader - Origin = Host —
Originmust matchHost - No embedded credentials — URLs with
user:pass@hostare blocked - Valid protocol — only
http:andhttps:
Bypass
Section titled “Bypass”| Case | When |
|---|---|
Header X-Buntime-Internal: true | Worker → runtime (internal) |
GET, HEAD, OPTIONS | Non-mutating methods |
Errors
Section titled “Errors”HTTP/1.1 403 ForbiddenContent-Type: text/plain
Forbidden - Origin requiredor simply Forbidden when the origin does not match.
Request ID correlation
Section titled “Request ID correlation”Every request carries an X-Request-Id for tracing.
| Header | Direction | Description |
|---|---|---|
X-Request-Id | Request | Client may provide (optional) |
X-Request-Id | Response | Always present (auto-generated via crypto.randomUUID() if absent) |
The ID propagates through:
- Logs (all levels)
- Errors
- Workers (via internal header)
- Plugin hooks (
PluginContext.requestId)
Usage details in logs: Logging.
Reserved paths
Section titled “Reserved paths”Plugins cannot use the following as their base:
| Path | Reason |
|---|---|
/api | Runtime internal routes |
/health | Health checks |
/.well-known | Standardized URIs (ACME, security.txt, etc.) |
Attempting to register a plugin with base: /api aborts startup:
Error: Plugin "my-plugin" cannot use reserved path "/api". Reserved paths: /api, /health, /.well-knownPath validation
Section titled “Path validation”Plugin base path
Section titled “Plugin base path”Must match ^/[a-zA-Z0-9_-]+$:
- Starts with
/ - Only alphanumeric, underscore, and hyphen
- Single segment (no nested
/)
| Invalid | Why |
|---|---|
/plugins/my-plugin | Nested path |
/my plugin | Space |
my-plugin | No leading slash |
Entrypoint path traversal
Section titled “Entrypoint path traversal”Worker entrypoints are resolved against APP_DIR to prevent traversal:
const resolvedEntry = resolve(APP_DIR, ENTRYPOINT);if (!resolvedEntry.startsWith(APP_DIR)) { throw new Error("Security: Entrypoint escapes app directory");}Blocks ../../etc/passwd, /absolute/path/outside/app, etc.
Worker collision
Section titled “Worker collision”The pool prevents duplicate registrations of the same app@version from different directories:
Error: Worker collision: "my-app@1.0.0" already registered from "/apps/my-app", cannot register from "/other/my-app"Prevents accidental duplicate deploys and potential route hijacking.
Sensitive env var filtering
Section titled “Sensitive env var filtering”When manifest.yaml declares env: to pass variables to the worker, “sensitive” variables are automatically blocked.
Blocked patterns
Section titled “Blocked patterns”| Pattern | Examples |
|---|---|
^(DATABASE|DB)_ | DATABASE_URL, DB_HOST |
^(API|AUTH|SECRET|PRIVATE)_?KEY | API_KEY, SECRET_KEY |
_TOKEN$ | ACCESS_TOKEN, GITHUB_TOKEN |
_SECRET$ | JWT_SECRET, CLIENT_SECRET |
_PASSWORD$ | DB_PASSWORD, ADMIN_PASSWORD |
^AWS_ | AWS_ACCESS_KEY_ID |
^GITHUB_ | GITHUB_TOKEN |
^OPENAI_ | OPENAI_API_KEY |
^ANTHROPIC_ | ANTHROPIC_API_KEY |
^STRIPE_ | STRIPE_SECRET_KEY |
When a variable is blocked, a WARN log is generated:
WRN Blocked sensitive env vars from worker {"blocked":["DATABASE_PASSWORD","API_KEY"]}Env vars inherited by the worker
Section titled “Env vars inherited by the worker”The wrapper passes a controlled set:
| Variable | Source |
|---|---|
APP_DIR | Runtime (absolute path) |
ENTRYPOINT | Runtime (full path) |
NODE_ENV | Inherited |
RUNTIME_API_URL | Runtime (internal URL) |
RUNTIME_LOG_LEVEL | Inherited |
RUNTIME_PLUGIN_DIRS | Inherited |
RUNTIME_WORKER_DIRS | Inherited |
WORKER_CONFIG | Runtime (JSON) |
WORKER_ID | Runtime (UUID) |
Custom from manifest.env | Filtered by the patterns above |
To pass secrets to a worker securely, use plugins (turso, keyval) with ${VAR} interpolation in the plugin manifest — not manifest.env on the worker.
Secure auto-install
Section titled “Secure auto-install”Workers with autoInstall: true in manifest.yaml run the install with strict flags:
bun install --frozen-lockfile --ignore-scripts| Flag | Purpose |
|---|---|
--frozen-lockfile | Does not modify the lockfile (reproducibility) |
--ignore-scripts | Does not run postinstall (prevents malicious code) |
Body and header limits
Section titled “Body and header limits”Request body
Section titled “Request body”| Limit | Value | Configurable |
|---|---|---|
| Default | 10 MB | Per worker via maxBodySize in the manifest |
| Maximum | 100 MB | Global ceiling (workers that exceed it are capped, generates WARN) |
Exceeded? 413 Payload Too Large.
Response headers (from worker to client)
Section titled “Response headers (from worker to client)”Applied in the wrapper to prevent memory exhaustion:
| Limit | Value | Description |
|---|---|---|
MAX_COUNT | 100 | Maximum number of headers |
MAX_TOTAL_SIZE | 64 KB | Total size of all headers |
MAX_VALUE_SIZE | 8 KB | Maximum size per value |
Headers exceeding the limit are truncated or ignored.
HTML injection prevention
Section titled “HTML injection prevention”When the runtime injects <base href> into HTML responses (for SPAs under a subpath), the value is escaped:
function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .replace(/\\/g, "\\\\");}Prevents XSS via a manipulated X-Forwarded-Prefix header or base path.
Namespace-scoped access control
Section titled “Namespace-scoped access control”API keys carry a namespaces list alongside their role/permissions. A namespace is the npm-style @scope of a worker/plugin name (see worker-pool namespaces). It gates which @scope units a key may see and manage, independent of the permission set — a key can hold workers:install yet still be denied a deploy into a namespace it doesn’t own.
namespaces value | Meaning |
|---|---|
["*"] (default) | Full access — every namespace and unscoped units. This is the value for legacy keys and the runtime root key. |
["@example", "@example-org"] | Only these scopes. Cannot touch unscoped units (an unscoped resource requires *). |
- Stored as a JSON column on
api_keys; surfaced onApiKeyInfo/ApiKeyPrincipaland validated against/^@[a-z0-9][a-z0-9._-]*$/i(normalizeNamespaces). - The runtime root key and any key with role behaviour
isRootbypass the namespace gate entirely (principalCanAccessNamespaceshort-circuits onisRoot).
Enforcement points
Section titled “Enforcement points”The namespace of a target is derived per surface, then checked with principalCanAccessNamespace(principal, ns):
| Surface | Where the namespace comes from | Behaviour |
|---|---|---|
/api/workers/:scope/:name/..., /api/plugins/:name (enable/disable/delete) | URL path (:scope, decoded plugin name) | API middleware (app.ts) returns 403 NAMESPACE_DENIED before the route runs. |
/api/{workers,plugins}/files/* (the FileBrowser: list/upload/mkdir/delete/rename/move/download) | the path (query or request body) | fs.ts self-enforces — it cannot be gated in the middleware because the path arrives in the body. Listing a forbidden @scope 403s; mount-level listings are filtered so siblings the key can’t access are hidden. |
POST /api/{workers,plugins}/upload | the archive’s package.json name (known only after extraction) | the upload handler 403s after readPackageInfo if the package’s @scope is out of bounds. |
GET /api/workers, GET /api/plugins, GET /api/plugins/loaded | each item’s name | results are filtered to the key’s namespaces. |
The principal is published on the Hono context (c.set("principal", …)) by the API gate and read by the sub-routers (c.get("principal")); the ContextVariableMap augmentation lives in apps/runtime/src/libs/hono-context.ts. Hono propagates context variables across app.route() mounts, so a single set in the gate reaches every handler.
cpanel
Section titled “cpanel”The key-create Sheet (/cpanel/keys) has a Namespaces field (default *, space/comma-separated); the keys table shows each key’s namespaces; the session principal exposes its own list. A restricted key only sees its namespaces’ workers/plugins and the FileBrowser hides folders it cannot access.
Best practices
Section titled “Best practices”For deploy
Section titled “For deploy”- HTTPS always — TLS terminated at the Ingress (cert-manager + Let’s Encrypt) or at the Route (OpenShift)
- Secure headers — configure CSP, HSTS, X-Frame-Options in the reverse proxy/ingress
- Rotate API keys —
buntime.masterKeyand CLI/TUI tokens - Monitor logs — specific
WARN/ERRORentries: sensitive env vars blocked, body capped, CSRF failed - Keep Bun and dependencies up to date — bump Bun and core plugins via
bump-version.ts - LibSQL token — in production, always use
DATABASE_LIBSQL_AUTH_TOKEN(notSQLD_DISABLE_AUTH=true)
For plugin authors
Section titled “For plugin authors”- Do not hardcode secrets — use
${VAR}interpolation in the manifest - Validate input — always use Zod or manual validation in public handlers
- Be careful with
publicRoutes— only expose routes that truly need to bypass auth - Rate limiting — use plugin-gateway instead of rolling your own
For worker/app authors
Section titled “For worker/app authors”- Do not store secrets in code — use
manifest.env(with automatic filtering) or plugins - Validate origins — for sensitive actions, check
Referer/Origin - Parameterized queries — prevent SQL injection when using the Turso/LibSQL integration
- Escape output — prevent XSS in HTML responses
Cross-refs
Section titled “Cross-refs”/datadirectories and lookup order: Environments- WARN/ERROR logs: Logging
- Manifest validation at startup: Environments
- Runtime root key: Helm charts (
buntime.rootKey)