Skip to content

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.

The runtime enforces CSRF (Cross-Site Request Forgery) validation on state-mutating methods.

POST, PUT, PATCH, DELETE.

  1. Origin required — protected methods must include an Origin header
  2. Origin = HostOrigin must match Host
  3. No embedded credentials — URLs with user:pass@host are blocked
  4. Valid protocol — only http: and https:
CaseWhen
Header X-Buntime-Internal: trueWorker → runtime (internal)
GET, HEAD, OPTIONSNon-mutating methods
HTTP/1.1 403 Forbidden
Content-Type: text/plain
Forbidden - Origin required

or simply Forbidden when the origin does not match.

Every request carries an X-Request-Id for tracing.

HeaderDirectionDescription
X-Request-IdRequestClient may provide (optional)
X-Request-IdResponseAlways 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.

Plugins cannot use the following as their base:

PathReason
/apiRuntime internal routes
/healthHealth checks
/.well-knownStandardized 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-known

Must match ^/[a-zA-Z0-9_-]+$:

  • Starts with /
  • Only alphanumeric, underscore, and hyphen
  • Single segment (no nested /)
InvalidWhy
/plugins/my-pluginNested path
/my pluginSpace
my-pluginNo leading slash

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.

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.

When manifest.yaml declares env: to pass variables to the worker, “sensitive” variables are automatically blocked.

PatternExamples
^(DATABASE|DB)_DATABASE_URL, DB_HOST
^(API|AUTH|SECRET|PRIVATE)_?KEYAPI_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"]}

The wrapper passes a controlled set:

VariableSource
APP_DIRRuntime (absolute path)
ENTRYPOINTRuntime (full path)
NODE_ENVInherited
RUNTIME_API_URLRuntime (internal URL)
RUNTIME_LOG_LEVELInherited
RUNTIME_PLUGIN_DIRSInherited
RUNTIME_WORKER_DIRSInherited
WORKER_CONFIGRuntime (JSON)
WORKER_IDRuntime (UUID)
Custom from manifest.envFiltered 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.

Workers with autoInstall: true in manifest.yaml run the install with strict flags:

Terminal window
bun install --frozen-lockfile --ignore-scripts
FlagPurpose
--frozen-lockfileDoes not modify the lockfile (reproducibility)
--ignore-scriptsDoes not run postinstall (prevents malicious code)
LimitValueConfigurable
Default10 MBPer worker via maxBodySize in the manifest
Maximum100 MBGlobal ceiling (workers that exceed it are capped, generates WARN)

Exceeded? 413 Payload Too Large.

Applied in the wrapper to prevent memory exhaustion:

LimitValueDescription
MAX_COUNT100Maximum number of headers
MAX_TOTAL_SIZE64 KBTotal size of all headers
MAX_VALUE_SIZE8 KBMaximum size per value

Headers exceeding the limit are truncated or ignored.

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, "&amp;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\\/g, "\\\\");
}

Prevents XSS via a manipulated X-Forwarded-Prefix header or base path.

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 valueMeaning
["*"] (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 on ApiKeyInfo / ApiKeyPrincipal and validated against /^@[a-z0-9][a-z0-9._-]*$/i (normalizeNamespaces).
  • The runtime root key and any key with role behaviour isRoot bypass the namespace gate entirely (principalCanAccessNamespace short-circuits on isRoot).

The namespace of a target is derived per surface, then checked with principalCanAccessNamespace(principal, ns):

SurfaceWhere the namespace comes fromBehaviour
/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}/uploadthe 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/loadedeach item’s nameresults 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.

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.

  1. HTTPS always — TLS terminated at the Ingress (cert-manager + Let’s Encrypt) or at the Route (OpenShift)
  2. Secure headers — configure CSP, HSTS, X-Frame-Options in the reverse proxy/ingress
  3. Rotate API keysbuntime.masterKey and CLI/TUI tokens
  4. Monitor logs — specific WARN/ERROR entries: sensitive env vars blocked, body capped, CSRF failed
  5. Keep Bun and dependencies up to date — bump Bun and core plugins via bump-version.ts
  6. LibSQL token — in production, always use DATABASE_LIBSQL_AUTH_TOKEN (not SQLD_DISABLE_AUTH=true)
  1. Do not hardcode secrets — use ${VAR} interpolation in the manifest
  2. Validate input — always use Zod or manual validation in public handlers
  3. Be careful with publicRoutes — only expose routes that truly need to bypass auth
  4. Rate limiting — use plugin-gateway instead of rolling your own
  1. Do not store secrets in code — use manifest.env (with automatic filtering) or plugins
  2. Validate origins — for sensitive actions, check Referer/Origin
  3. Parameterized queries — prevent SQL injection when using the Turso/LibSQL integration
  4. Escape output — prevent XSS in HTML responses