0005 — No paths across IPC, and capability-gated commands

Status: Accepted · 2026-08-20

Context

The interface is a webview rendering PDFs that arrive by email from subcontractors. Two questions follow: what can a compromised renderer reach, and what can a hostile document do?

Decision

The renderer never names a file

There is no command that takes a path, no filesystem plugin, no shell access and no generic bridge command. Where a file must be chosen, the native picker runs on the Rust side and only an opaque id crosses back.

The obvious alternative — open_file(path) plus a scope check — was rejected because a scope check is a filter on an attacker-controlled string, and the history of that pattern is a history of bypasses: normalisation differences, symlinks, UNC paths, 8.3 short names. Not accepting the string at all removes the class.

The cost is real: drag-and-drop of a PDF onto the window is disabled in this version, because it would hand the webview a path. It returns when the drop is handled entirely in Rust.

The command surface is enumerable

About eighteen named commands, listed in one file. Each validates its payload, authorises the act against the capability model, calls a domain use case, and maps the typed result. Business rules are not in them.

Capabilities are declared, not coded

Three capability files, each a few lines, saying exactly which plugin permissions the window holds. opener is restricted to http and https, so a hyperlink in a drawing goes to the system browser and can never navigate the application or reach a custom scheme handler.

Note the boundary honestly: Tauri's ACL governs core and plugin commands. SheetForge's own commands are gated by its own capability model in sf-security, checked in Rust before the act, with refusals written to the audit trail. Hiding a button is not a permission — the store is reachable from an import, a migration and a host script, so the check lives where every path crosses.

Documents are hostile input

Sniffed before anything is written; bounded on size, page count, decompressed size, archive entries, concurrent jobs and per-job time; parsed off the UI thread in cancellable work; never trusted for their own claims about themselves.

Errors carry no paths

Every message that crosses the boundary, reaches a log or lands in a diagnostic bundle is built to be safe to show and safe to keep, and there are tests asserting it — because this is exactly the kind of guarantee that decays silently as messages get improved.

What it costs

What would reverse it

Nothing foreseen. A managed deployment would substitute the role source, not remove the check.