Skip to main content
The second security gate decides which values a granted command may use. It has two layers that both run before the backend touches DuckDB: declarative scope entries attached to a capability, and the mandatory init-time Rust Policy. See Security overview for how this fits with permissions.

Scope entries (per-capability)

A capability can attach a scope of Entry objects to constrain command arguments. Each Entry has two optional fields (camelCase in JSON):
string
Allow a connection string — checked by load. Matches exactly.
string
Allow a filesystem path prefix — checked by import’s source and export’s path. Matches by whole path component and is traversal-safe.
These are attached to a permission in the capability file via allow / deny arrays — see Capabilities & permissions.

Enforcement: per-field and deny-first

Enforcement is evaluated independently per field:
1

Deny wins

A matching deny entry for the field always rejects.
2

Allow requires a match

If any allow entries exist for that field, the value must match one of them.
3

Empty allow is unconstrained

An empty allow list for a field means that field is unconstrained by the capability — the init-time Policy remains the mandatory layer.
Per-field independence has a consequence: a capability that declares only path entries does not restrict connection, and vice-versa. If you mean to constrain both, populate both fields. Declaring a path allow-list does not implicitly lock down which databases open.

Matching rules

  • connection matches by exact string equality — duckdb:app.db allows only that exact string.
  • path matches by whole-component prefix and is traversal-safe:
    • a .. segment anywhere in the value is rejected outright, so data/ never allows data/../secret;
    • matching is on whole components, so data allows data/x but not database/x or data_secret/y;
    • backslashes are treated as separators (Windows paths).
The scope schema is declared in scope::Entry and registered from build.rs, so capability files get validation and autocompletion for these fields.

The Rust Policy

The Policy is configured on core::Builder / diesel::Builder at plugin init and applies to every operation regardless of capabilities — it is the mandatory floor beneath the per-window scope. Its default is fully permissive; opt into each restriction.

The risky-statement filter

deny_risky_statements(true) rejects a statement whose leading keyword is one of:
These reach the filesystem/network, load code, or mutate session/catalog state out of band.
This is a coarse, leading-keyword-only filter. It does not parse the statement, so a read that reaches disk through a table function — SELECT * FROM read_csv_auto('/etc/passwd') — passes it (the leading keyword is SELECT). This is why select must be contained with read_only and a connection allow-list, not this filter alone. See the select caveat.

Path normalization

allow_path canonicalizes both the allowed directories and the candidate path, then checks the candidate starts with an allowed directory. For a not-yet-created export target, it falls back to canonicalizing the parent directory and rejoining the filename — so writing a new file inside an allowed directory still resolves.

Network-extension gating

load_extension is gated by allow_network: while it is off (default), the network extensions httpfs, aws, and azure are rejected (they enable SQL-driven network egress / SSRF / exfiltration). Non-network extensions (spatial, json, vss, …) load regardless.

How the two layers combine

For a given command invocation:
  1. The capability must grant the command (permissions gate).
  2. The capability’s scope entries for the relevant field must allow the value (or be empty).
  3. The init-time Policy must allow it.
All three must pass. A failure at step 2 or 3 surfaces as denied by policy: ….
Scope and Policy are complementary, not redundant. Scope is declarative and per-window (an app can grant different windows different connections); Policy is mandatory and global (it cannot be widened by a capability). Use scope to differentiate windows; use Policy as the floor no window can escape.

See also