Plugin sandbox
This page walks through the security model behind mode: isolated, for anyone deciding whether to
trust a plugin, or writing one that others will trust. Just want the short version? See
Trust and permissions in the plugins guide instead.
Steno supports two explicit plugin execution modes:
trustedis the default for compatibility. The plugin is imported into the Steno process and inherits all of its permissions.isolatedruns the plugin in a dedicated Deno subprocess with runtime capabilities denied unless they are explicitly granted.
plugins:
- package: jsr:@example/[email protected]
mode: isolated
timeoutMs: 5000
maxOutputBytes: 4194304
memoryMb: 128
lockFile: ./deno.lock
permissions:
read:
- ./content
env:
- PUBLIC_SITE_URL
net:
- api.example.com:443
import:
- jsr.io:443
Registry plugins in isolated mode must include an explicit version. HTTP(S) plugins also require a
sha256-<base64> integrity value. Every isolated remote plugin runs with a frozen Deno lockfile
(./deno.lock by default), protecting its complete remote dependency graph.
Security boundary
Each isolated plugin gets its own process. Steno communicates with it using a versioned JSON-lines
protocol over stdin/stdout. Plugin factories and these hooks execute inside the worker:
beforeBuild, transformAst, transformHtml, afterPage, and afterBuild.
The worker starts with:
- a cleared process environment;
- filesystem read and write denied;
- runtime network access denied;
- environment access denied;
- subprocess execution denied;
- FFI denied;
- OS/system information denied;
- remote module imports limited to required and configured hosts;
- no interactive permission prompts;
- a configurable V8 heap ceiling;
- per-request input and output limits; and
- a deadline for initialization and every hook call.
The worker is terminated when it times out, crashes, emits malformed or oversized protocol data, throws from a hook, completes a build, or when the parent build exits early. A worker failure fails the build but cannot terminate the Steno process.
API consumers can call steno.cancel() to terminate active isolated-plugin workers. An in-flight
isolated hook rejects and the build fails cleanly.
Filesystem permission for a local file:// plugin always includes its top-level module file so Deno
can import it. Local transitive modules need explicit read grants. Runtime Node compatibility APIs
are subject to the same Deno permissions: importing node:fs does not grant filesystem access, and
importing node:child_process does not grant subprocess access.
Threat model
The sandbox is built to contain a malicious plugin that tries to:
- read or modify project and host files;
- read secrets from environment variables;
- open network connections;
- execute child processes;
- load native libraries through FFI;
- inspect protected operating-system information;
- reach those capabilities indirectly through Node compatibility APIs;
- hang a build with an infinite loop;
- exhaust the V8 heap;
- flood the parent with output;
- corrupt the protocol; or
- crash or exit its worker.
Anything you explicitly list under permissions is intentionally outside the boundary. A plugin
allowed to write a directory can corrupt files in that directory. A plugin allowed network access
and a secret-bearing environment variable can exfiltrate it. So keep your grants narrow, only give a
plugin what it genuinely needs.
Current exclusions
We don't claim these are sandboxed:
- Plugins configured with
mode: trustedor the string shorthand. - Theme modules, Tau templates, and theme-bundled plugins. Themes are loaded and rendered
in-process. Set
allowThemePlugins: falseto disable bundled plugins, but only install themes you trust. - The Steno parent process itself.
- Denial of service outside the worker's V8 heap, such as kernel or Deno runtime vulnerabilities.
- Vulnerabilities in Deno's permission implementation, V8, the operating system, or native code explicitly allowed by the user.
- Integrity of an entire remote dependency graph unless a frozen lockfile is used.
Treat the sandbox as defense in depth for now, not a guarantee, until it's passed cross-platform adversarial testing and an independent security review.