3.7 KiB
checker/fs — filesystem allowlist
(no submodules)
Purpose
This module owns the permissions.fs[] enforcement path. Every
filesystem capability call routed through the sandbox host MUST pass
through checkFs before the underlying I/O runs. The check is
synchronous, deterministic, and O(rules) — never network, never disk,
no closure state beyond the compiled rule list on the policy.
The function is the only enforcement layer in the package that has to
defend against path-traversal: a manifest rule like ./uploads/* is
intended to scope access to the uploads dir, and a request supplying
./uploads/../etc/passwd MUST resolve to ./etc/passwd and be denied
even though the literal string still starts with ./uploads/.
Features
checkFs(policy, path, mode)— returns{ outcome: "allow" },{ outcome: "deny", reason: "path-not-in-allowlist" }, or{ outcome: "deny", reason: "mode-not-permitted" }.- Mode-aware — read access defaults are wide, write access requires
the rule to be authored as
rw:<glob>. - Traversal-safe — paths are normalised through the policy's
normalisePathhelper before the rule's compiled regex sees them.
Usage: Basic
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
import { checkFs } from "@suaveplan/api-sandbox-permissions/checker";
const policy = new PermissionPolicy({
permissions: { fs: ["./uploads/*"] },
});
checkFs(policy, "./uploads/avatar.png", "read"); // { outcome: "allow" }
checkFs(policy, "./etc/passwd", "read");
// { outcome: "deny", reason: "path-not-in-allowlist" }
Usage: Advanced
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
import { checkFs } from "@suaveplan/api-sandbox-permissions/checker";
import {
SAND_PERM_CODES,
SandboxPermissionError,
} from "@suaveplan/api-sandbox-permissions/error-codes";
const policy = new PermissionPolicy({
permissions: {
fs: ["rw:./uploads/**", "./readonly/*.txt"],
},
});
function gatedFsCall(
plugin: string,
path: string,
mode: "read" | "write"
): void {
const result = checkFs(policy, path, mode);
if (result.outcome === "deny") {
throw new SandboxPermissionError(
SAND_PERM_CODES.DENIED,
"fs capability denied",
{ context: { capability: "fs", reason: result.reason } }
);
}
}
gatedFsCall("billing", "./uploads/temp/log.txt", "write"); // allowed by rw: rule
gatedFsCall("billing", "./readonly/config.txt", "read"); // allowed read-only
Hosts MAY also wire checkFs directly into the worker capability proxy
without throwing — when the result is Deny, the host fabricates a
{ ok: false, error: ... } envelope so the worker boundary never sees
a stack trace from the main process.
API Reference
checkFs(policy: PermissionPolicy, path: string, mode: FsAccessMode): CheckResultmode = "read" | "write"CheckResult = { outcome: "allow" } | { outcome: "deny"; reason: string }reasonis one of"path-not-in-allowlist","mode-not-permitted".
Implementation Notes
The literal-root check (startsAtRoot) is what defends against ..
escape: a rule like ./uploads/* has root = "./uploads", and a
resolved input that doesn't start with "./uploads" (or equal it)
fails the gate before the regex runs.
The mode check is a soft veto inside the loop — a write attempt against
a read-only matching rule sets modeBlocked and keeps scanning, so a
later rw: rule on the same path still wins. Only when every matching
rule was read-only AND write was requested does the deny reason become
"mode-not-permitted". This keeps the rule list order-insensitive.