first commit
This commit is contained in:
104
docs/api-stack/api/api-sandbox-permissions/src/checker/fs/fs.md
Normal file
104
docs/api-stack/api/api-sandbox-permissions/src/checker/fs/fs.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# `checker/fs` — filesystem allowlist
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
`normalisePath` helper before the rule's compiled regex sees them.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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): CheckResult`
|
||||
- `mode = "read" | "write"`
|
||||
- `CheckResult = { outcome: "allow" } | { outcome: "deny"; reason: string }`
|
||||
- `reason` is 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.
|
||||
Reference in New Issue
Block a user