Skip to main content
The webview is untrusted. Anything running in it — your own code, a dependency, injected content — can call any command the capability grants. And DuckDB SQL can reach the filesystem and network through table functions, ATTACH, extensions, and COPY. Design the plugin’s permissions and policy as if the webview were hostile.

The threat model

DuckDB is not a sandbox. A single select can read arbitrary files; load_extension can pull a network extension; export and COPY can write files. The plugin’s job is to stand between the untrusted webview and that reach, and it does so with two independent gates, both checked before the backend touches DuckDB:

Capabilities & permissions

Which commands the webview may invoke at all. Granted per-window in capability files.

Scope + Policy

Which values those commands may use — connections, paths, statements, extensions. A declarative per-capability scope plus a mandatory init-time Rust Policy.
A command runs only if both gates pass: the capability must grant it, and the scope + policy must allow the specific connection string / path / statement / extension involved.

Defense in depth

Neither gate alone is sufficient; they cover different axes:
  • Permissions stop a command from being callable, but say nothing about its arguments.
  • Scope (capability-declared) narrows connection strings and paths per window, declaratively.
  • Policy (Rust, init-time) is the mandatory floor — it applies to every operation regardless of capabilities, and covers read-only mode, connection allow-lists, path allow-lists, the risky-statement filter, and network-extension gating.

The select caveat — read this

select is in the default permission set, but DuckDB SELECT can still reach the filesystem through table functions (read_csv_auto, read_parquet, glob, …). The deny_risky_statements filter only inspects the leading keyword, so a read like SELECT * FROM read_csv_auto('/etc/passwd') passes it.
Treat select (and query_arrow and stream, which also forward arbitrary read SQL) as filesystem-read-capable. For an untrusted webview, defend in depth:
  • read_only(true) plus an allow_connection(…) allow-list, and/or
  • a capability scope that pins connection,
  • keep allow_network(false) so network extensions cannot load.
A fully sandboxed read-only mode (disabling DuckDB external file access) is a planned follow-up. Until then, the layered policy above is the recommended containment.

How denials surface

Both gates reject before the backend runs. A policy or scope denial surfaces to the webview as the plugin error string denied by policy: …, which rejects the invoke promise. A capability that doesn’t grant a command fails at the Tauri IPC layer before the plugin is even reached.

Safe-defaults checklist

1

Start from the read-only default

Begin with better-duck-tauri:default (load + close + select); add allow-execute only where writes are truly needed.
2

Run read-only where possible

read_only(true) for any connection the webview only reads.
3

Pin connections

allow_connection(…) plus a scope connection entry to pin which databases open.
4

Pin paths

allow_path(…) plus a scope path entry for import/export directories.
5

Block risky statements

deny_risky_statements(true) to reject ATTACH / INSTALL / COPY / … via select / execute.
6

Keep the network closed

Leave allow_network(false) unless a feature genuinely needs httpfs / aws / azure.

See also