Audit remediation 20261018: OH-6.5 root-normalization P0 + embedded-placeholder P1 (programmer fix, QA ACCEPT, VERDICT PASS)

This commit is contained in:
John Smith committed 2026-10-18 05:15:00 -04:00
1 parent 551e5feba2
commit 1d17b16618
5 files changed
+522 -25

No files matched your search

@@ -0,0 +1,125 @@
# QA Red-Proof Evidence Log — OH-6.5-root-normalization (P0) + OH-6.5-embedded-placeholder (P1)
- Branch: `fix/oh-6-5-root-normalization-embedded-placeholder`
- Worktree: `/home/john/Projects/Heretek-AI-Rebuild/opencode-harness/worktrees/audit-remediation`
- QA date (UTC): 2026-10-18
- Base commit: `551e5fe` (Audit remediation 20261018, 1.0.11)
- Full-context reads completed: AGENTS.md, SPEC.md, AUDIT_REPORT_FINDINGS.md
(§OH-6.5-root-normalization lines 77-139 + §OH-6.5-embedded-placeholder
lines 141-172, verbatim), programmer's report
`docs/reports/audit-remediation-20261018-root-normalization.md`,
`src/guards/shell-parse.ts`, `test/guards.test.ts`, `docs/prompts/verify-stage.md`.
## 1. Acceptance gates (exact commands + exit codes, from inside the worktree)
| # | Command | Exit | Result |
|---|---------|------|--------|
| 1 | `bun run check` (tsc --noEmit) | 0 | PASS |
| 2 | `bun test test/guards.test.ts` (stock TMPDIR) | 0 | PASS — 41 pass, 0 fail, 497 expects (was 32 tests; +9 new) |
| 3 | `bun test` (stock TMPDIR) | 1 | FAIL *solely* on host `/tmp` inode-exhaustion (ENOSPC on `mkdtemp /tmp/*`); 302 pass / 65 fail, every failure is `ENOSPC: no space left on device, mkdtemp '/tmp/…'` — no assertion failure. Documented host condition, matches audit baseline. |
| 4 | `TMPDIR=/dev/shm/qa-tmp bun test` (override, override dir pre-created) | 1 | 365 pass / 2 fail. Both failures are the audit-documented TMPDIR-flip pair (`OH-6.5-redirect-escape` OS-temp allow + `OH-6.5-cp-mv-argv-targets` absolute-source allow) which pass with stock TMPDIR. |
| 5 | `TMPDIR=/dev/shm/qa-tmp bash scripts/accept/wave-1.sh` | 0 | PASS — `WAVE 1 ACCEPT: ALL GATES GREEN` (84 tests, zero-stub/scope scans clean) |
| 6 | `TMPDIR=/dev/shm/qa-tmp make accept WAVE=1` | 0 | PASS — `WAVE 1 ACCEPT: ALL GATES GREEN` |
Pre-existing proof for the 2 TMPDIR-flip failures (git stash A/B):
- `git stash push -- src/guards/shell-parse.ts test/guards.test.ts` (pristine tree) +
`TMPDIR=/dev/shm/qa-tmp bun test` → 356 pass / **2 fail** (same 2 tests), exit 1.
- `git stash pop` → programmer changes restored (`M src/guards/shell-parse.ts`,
`M test/guards.test.ts`).
- Conclusion: the 2 failures are environmental (documented `/tmp` ENOSPC workaround
flips the `tmpdir()` allowlist at `permission-guard.ts:522`), NOT regressions.
Matches the audit's own note ("both pass with stock TMPDIR") and the programmer's
report stash A/B.
## 2. Independent probe matrix (public guard API, no code change)
Probe driver: `bun -e` importing `classifyShellCommand` /
`isDestructiveShellCommand` (`src/guards/shell-parse.ts`), `evaluateBoardGuard`
+ `defaultGuardConfig` (`src/guards/permission-guard.ts`), `evaluateTaintGuard`
+ `recordUntrustedContentTaint` + `clearTaintState` (`src/guards/taint-guard.ts`).
Roles probed per row: builder, architect, director, qa-adversary, agency-qa
(+ tainted-session layer per row). Exit 0, `PROBE-MATRIX: ALL EXPECTED`.
ATTACK rows — every one: `class=destructive`, `destr=true`,
`DENY:rule-1-destructive` for all 5 roles, `taint=DENY:TAINT-DESTRUCTIVE-1`:
- `rm -rf //`, `rm -rf ///`, `rm -rf /.`, `rm -rf /./`, `rm -rf /../`,
`rm -rf /tmp/../`, `rm -rf ~root`, `rm -rf ~root/.ssh`
- `find // -exec rm -rf {} \;`, `find /./ -exec rm -rf {} \;`,
`find /tmp/../ -exec rm -rf {} \;`, `find .. -exec rm -rf {} \;`,
`find ~root -exec rm -rf {} \;`
- `find // -delete`, `find /tmp/../ -delete`, `find .. -delete`
- `find / -exec rm -rf {}/foo \;`, `find / -exec rm -rf {}foo \;`,
`find / -exec rm -rf prefix{} \;`, `find / -exec rm -rf -- {}/foo \;`
- `find / -ok rm -rf {}/foo \;`, `find / -ok rm -rf {}foo \;`,
`find / -ok rm -rf prefix{} \;`
CONTROL rows — every one: `class=write`, builder `ALLOW` (no over-blocking):
- `rm -rf /tmp`, `rm -rf /tmp/wave1-abcd`, `rm -rf /var/tmp/app`
- `find /tmp -name x -exec rm -rf {} \;`,
`find /tmp -name x -exec rm -rf {}/foo \;`,
`find . -exec rm -rf {} \;`, `find . -exec rm -rf {}/foo \;`,
`find . -name x -exec rm -rf {}/foo \;`, `find /tmp -name x -delete`
## 3. Red-proof sabotage log (distinct per invariant)
Restore method note: the mandated `git checkout -- <file>` would destroy the
programmer's uncommitted remediation (the file carries the fix under test), so
each sabotage was applied as a minimal `edit` on top of the remediated file and
reverted with the exact inverse `edit`; restoration is proven by byte-identical
sha256 vs the pre-sabotage hash plus `git status`/`git diff --stat` showing only
the intended programmer modifications. Pre-sabotage hashes:
`shell-parse.ts = 958708d5a426e47fa012e0c64070afb3078cc399038c376c330e0e82dd5c7c26`,
`guards.test.ts = c3e877e58c692905d1f0a55dec49f7d8885eccc2d29fd34f79be1269bac49e48`.
- Sabotage A (OH-6.5-embedded-placeholder invariant):
file:line `src/guards/shell-parse.ts:905`,
before `if (a.includes("{}")) {` → after `if (a === "{}") {`.
Suite: `bun test test/guards.test.ts` → **38 pass / 3 fail, EXIT 1** (RED —
the 3 embedded-`{}` guard tests fail, incl. the taint-layer test).
Reverted via inverse edit; post-restore sha256 `958708d5…` identical;
`bun test test/guards.test.ts` → 41 pass / 0 fail, EXIT 0 (GREEN).
- Sabotage B (OH-6.5-root-normalization invariant):
file:line `src/guards/shell-parse.ts:981` (`normalizeWipeTarget`),
before `return posix.normalize(s.replace(…).replace(/\/+/g, "/"));`
→ after `return s.replace(…).replace(/\/+/g, "/");`
(slash-collapse only, no `..`/root resolution — exact-match-only behavior).
Suite: `bun test test/guards.test.ts` → **38 pass / 3 fail, EXIT 1** (RED —
the 3 root-normalization guard tests fail, incl. the taint-layer test).
Reverted via inverse edit; post-restore sha256 `958708d5…` identical;
`bun test test/guards.test.ts` → 41 pass / 0 fail, EXIT 0 (GREEN).
Both checks are non-vacuous: each breaks RED under targeted sabotage and returns
GREEN on restoration. `git status --short` after each restore shows only:
`M src/guards/shell-parse.ts`, `M test/guards.test.ts`,
`?? docs/reports/audit-remediation-20261018-root-normalization.md`
(+ this evidence file) — no sabotage residue in tracked production files.
## 4. Stub / shortcut scan (changed files)
- `grep -rnE 'TODO|\|\| true|set \+e|cmd: "true"|test\.skip|describe\.skip|it\.skip|catch *\(.*\) *\{\}' src/guards/shell-parse.ts test/guards.test.ts` → zero hits (exit 1).
- `return true` occurrences audited: all are genuine guard-predicate outcomes
(destructive/write/allow decisions with surrounding logic), none vacuous.
- Wave-1 accept zero-stub scan (covers all of `src/` + `test/`) → clean (gate 5 above).
- No `|| true`, `set +e`, `cmd: "true"`, `test.skip`, empty catch, dummy passes.
## 5. Scope compliance
- No WebUI/HTTP-server/fleet/DAG/worktree/research-engine code in the changed
files. The only grep hits for scope terms are `scripts/accept/wave-1.sh`'s own
scope-refusal gate (comments + its grep pattern), which passes green.
- Single-session in-process OpenCode v2 plugin shape preserved; no new
dependencies (fix uses stdlib `node:path` only).
- Net `src/` delta stays under the OH-scope-loc-budget hard caps per the
programmer's report (raw 11981 ≤ 12000, stripped 9975 ≤ 10000; cap test green
under the TMPDIR override run).
## QA verdict
All verify-the-fix criteria from AUDIT_REPORT_FINDINGS.md hold: every Raw-output
probe row now classifies `destructive` + denies `rule-1-destructive` for
builder/architect/director/qa (+ agency-qa) and `TAINT-DESTRUCTIVE-1` while
tainted; all scoped controls stay `write`/ALLOW; gates green (modulo the proven
pre-existing TMPDIR-flip pair); both sabotages RED then restore byte-identical;
zero stubs; scope clean.
QA ACCEPT
@@ -0,0 +1,73 @@
# Drift-Guard Verification: Audit Remediation 20261018 — OH-6.5-root-normalization (P0) + OH-6.5-embedded-placeholder (P1)
- **Auditor role:** `agency-drift-guard` (read-only; no production code changed by auditor)
- **Branch / worktree:** `fix/oh-6-5-root-normalization-embedded-placeholder` @ `/home/john/Projects/Heretek-AI-Rebuild/opencode-harness/worktrees/audit-remediation`
- **Base commit:** `551e5fe` (findings baseline)
- **Date (UTC):** 2026-10-18
- **Mandated reads (all read in full):** `AGENTS.md`, `docs/prompts/SPEC.md`, `docs/prompts/verify-stage.md`, `AUDIT_REPORT_FINDINGS.md` (§OH-6.5-root-normalization + §OH-6.5-embedded-placeholder), programmer report `docs/reports/audit-remediation-20261018-root-normalization.md`, target files `src/guards/shell-parse.ts`, `test/guards.test.ts`, `src/guards/permission-guard.ts`, `src/guards/taint-guard.ts`
## 1. Gates (exact commands + exit codes, from inside the worktree)
| Command | Result |
|---|---|
| `bun run check` (tsc --noEmit) | exit **0** |
| `bun test test/guards.test.ts` (stock TMPDIR) | **41 pass, 0 fail**, exit **0** (was 32; +9 new) |
| `TMPDIR=/dev/shm bun test` (full suite; override required — host `/tmp` inode-exhausted/ENOSPC) | **365 pass, 2 fail**, exit 1 |
| `TMPDIR=/dev/shm bash scripts/accept/wave-1.sh` | `WAVE 1 ACCEPT: ALL GATES GREEN`, exit **0** |
**2-fail exception — proven pre-existing environmental via git-stash A/B (not a regression):**
- With fix: `(fail) OH-6.5-redirect-escape … workspace-relative and OS-temp targets still allow`, `(fail) OH-6.5-cp-mv-argv-targets … legitimate absolute-source uses … allow` → 365 pass / 2 fail / 367 total.
- `git stash push -- src/guards/shell-parse.ts test/guards.test.ts` → pristine: **identical 2 fails** → 356 pass / 2 fail / 358 total.
- `git stash pop` → fix restored; `git status` shows only the 2 fix files modified. Both failures are the audit-documented TMPDIR-flip OS-temp tests (pass with stock TMPDIR, fail identically with `/dev/shm` TMPDIR on the pristine tree). The delta is exactly +9 new tests, +9 passes, zero new failures.
## 2. Stub / shortcut scan (src/ AND test/)
| Pattern | Result |
|---|---|
| `TODO` in `src/ test/` | **zero hits** (`grep` exit 1) |
| `\|\| true` | only `scripts/accept/wave-8.sh:81-82` (the accept script's own self-scan quoting the pattern, excluded by the script itself) — **zero real hits** |
| `set +e` | **zero hits** |
| `cmd: "true"` / `echo "passed"` | only `scripts/accept/wave-8.sh:84` (self-scan) — **zero real hits** |
| `test.skip`/`describe.skip`/`it.skip`/`xit(`/`xdescribe(` (word-boundary scan) | **zero hits** (naive scan hit `process.exit(0)` in `test/wave8-audit.test.ts:121` — a shell-command string inside a mutating-shell corpus, i.e. `exit(` false positive, not a skipped test) |
| `return true` in `src/` | 60+ hits, **all guarded predicates** (`isRootWipeTarget` fail-closed wipe checks, `findExecPayloadIsPlaceholderWipe` placeholder detection, `shellTargetEscapesWorkspace` containment, `isUntrustedIngestionTool` MCP arms, loopback/private-IP guards) — every one has branching logic; **zero vacuous passes** |
| empty `catch {}` / `catch (e) {}` | **zero hits** |
| Unwired exports | `normalizeWipeTarget` (shell-parse.ts:977) called by `isRootWipeTarget` (:1003); `isFindWipeRootOperand` (:847) called by `findHasWipeRoot` (:843) and `-delete` branch (:1164); `findPathOperands` reused by `-delete` branch. All wired into `classifyShellCommand` → `isDestructiveShellCommand` → `evaluateBoardGuard` Rule 1 + `evaluateTaintGuard` TAINT-DESTRUCTIVE-1. **Zero unwired.** |
ANY-hit rule: no unjustified hit → no FAIL.
## 3. KV-cache invariants spot-check
- `TMPDIR=/dev/shm bun test test/context-budget.test.ts` → **23 pass, 0 fail**, exit 0.
- `event.system` untouched, last-`role==="user"` append, lessons sorted by `id.localeCompare` with zero float scores/timestamps: covered green by that suite; no context-budget file touched by this fix (`git status` confirms).
## 4. Scope refusal check
- `getRecommendedMcpConfig()` (`src/ui-auditor/mcp-defaults.ts:30-42`) returns **ONLY** `lsp-mcp-server` (`bunx lsp-mcp-server`) + `fallow-mcp` (`npx --yes --package fallow fallow-mcp`). No extra servers.
- Broad grep hits for `worktree` in `src/` are upstream host plumbing (`contract-adapter.ts:205-206` `location.worktree` context fields, `index.ts:1093-1095` `ctx?.worktree` reads, interceptor comments about OpenCode built-in plugin instructions) — reads of host context, **not** a worktree manager. No `http.createServer`/`listen(`/WebUI/DAG-scheduler/research-engine code anywhere in `src/`. Wave-1 accept scope gate passes green (see §1).
## 5. Adversarial bypass hunt (beyond QA's matrix — 33 attacks + 9 controls, all via public API)
Probe: `/dev/shm/drift-bypass-probe.ts` (`classifyShellCommand` + `isDestructiveShellCommand` + `evaluateBoardGuard` builder-deny). Result: **33/33 DENY-OK, 9/9 ALLOW-OK, exit 0**.
Attacks denied (each `destructive` + `isDestructive===true` + builder `allowed===false`): `/././`, `////tmp//..//`, `/.//`, `/tmp//../`, `rm -rf / --no-preserve-root`, `rm --recursive --force /`, `rm -r -f /`, `rm -rf -- /`, quoted `'~root'`/`"~root"`, `$HOME`, `${HOME}/..`, `-execdir`/`-okdir` `{}` payloads, `+`-terminator `find / -exec rm -rf {} +` and `{}/foo +`, `prefix{}`, long-form `--recursive --force`, `rm -rf -- {}`, `../..` and `a/../../` find roots (±exec/−delete), `/tmp/../` + `{}/foo`, `sudo`/`exec`/`bash -c` wrappers, tab-separated `rm\t-rf\t/` and `find /\t-exec`, single/double-quoted `'{}'`/`"{}"` payloads.
Controls still allowed (each `write` + builder `allowed===true`, no over-blocking): `rm -rf /tmp`, `/tmp/wave1-abcd`, `/var/tmp/app`, `rm --recursive --force /tmp/app`, `find /tmp -name x -exec rm -rf {} \;`, `find /tmp -name x -exec rm -rf {}/foo \;`, `find . -exec rm -rf {} \;`, `find . -name x -delete`, `find /tmp -name x -delete`.
Refutation angles from the cross-examination notes attempted before upholding and defeated: `//` stat-identity family normalizes to `/`; rm-backstop evasion rejected (`find //` never lets `rm` see `/` — now caught at the find-root layer); `find ..` downgrade rejected (`..`-leading/segment roots are wipe roots in the same Rule-1 path); `~root` env-specificity rejected (fail-closed `~`-leading → deny); wrapper composition (`sudo`/`exec`/`bash -c`) strengthens the deny; `+`-terminator and `-okdir`/`-execdir` forms deny; no reliance on the non-filed `FIND`/`RM` spellings.
## 6. Independent sabotage test (distinct from QA's)
- **Sabotage (NOT touching fix files):** neutered permission-guard Rule 3a in `src/guards/permission-guard.ts:649` (`path.startsWith("src/")` → `path.startsWith("src-internal/")`), via file write (auditor tool-hygiene exception for sabotage only).
- **Result:** `bun test test/guards.test.ts` → **39 pass, 2 fail, exit 1** — fail-closed proven (suite is non-vacuous).
- **Restore:** `git checkout -- src/guards/permission-guard.ts` (safe: file carries no uncommitted fix) → sha256 `fcfffc06…` identical before/after; fix files untouched (`shell-parse.ts` `958708d5…`, `guards.test.ts` `c3e877e5…` identical); `git status --short` shows only `M src/guards/shell-parse.ts`, `M test/guards.test.ts`, `??` programmer report.
- **Post-restore:** `bun test test/guards.test.ts` → **41 pass, 0 fail, exit 0**.
## 7. Regression + LOC notes
- `test/guards.test.ts` diff: **+169 lines, zero deleted/relaxed assertions** (no `^-` lines in diff) — pure additions across the 9 new tests.
- `src/guards/shell-parse.ts` diff: +70/−25 — deletions are exactly the intended replacements (exact-match `isRootWipeTarget` body → normalized version incl. `~<user>` fail-closed; `a === "{}"` → `a.includes("{}")`; `-delete` exact-match branch → `findPathOperands(...).some(isFindWipeRootOperand)`), all in the fail-closed direction.
- `bash scripts/check-loc.sh` → `files=22 raw=11981 (budget 12000) stripped=9975 (budget 10000)`, `tracked (exit 0)` — gate passes; caps now 99.8%/99.75% full (next wave must budget accordingly; OH-scope-loc-budget overage fact unchanged, not worsened beyond +20 raw).
- Out-of-scope findings (OH-1.3/OH-1.1 tags+publish, OH-6.6 fork hook, OH-deps-caret, OH-1.1-no-gh-release) correctly untouched by the fix.
- Taint layer: no `taint-guard.ts` change needed — `evaluateTaintGuard` denies any `isDestructiveShellCommand` as `TAINT-DESTRUCTIVE-1`; classifier fix propagates automatically (proven by the 16-row + 4-row taint tests in the new suite).
VERDICT: PASS
@@ -0,0 +1,110 @@
# Programmer Report: Audit Remediation 20261018 — OH-6.5-root-normalization (P0) + OH-6.5-embedded-placeholder (P1)
- **Branch:** `fix/oh-6-5-root-normalization-embedded-placeholder`
- **Worktree:** `/home/john/Projects/Heretek-AI-Rebuild/opencode-harness/worktrees/audit-remediation`
- **Date (UTC):** 2026-10-18
- **Scope:** `src/guards/shell-parse.ts` (guard logic + comments only) and `test/guards.test.ts` (new regression tests only). No other production files touched; no version bump, tags, publishes, or CI changes.
## Defects remediated
### OH-6.5-root-normalization (P0)
`isRootWipeTarget` used exact-match only, so `rm -rf //`, `///`, `/.`, `/./`,
`~root`, `~root/.ssh` classified `write`/ALLOW, and `find // …`, `find /./ …`,
`find /tmp/../ …`, `find .. …`, `find ~root …` (`-exec rm -rf {} \;` and
`-delete` forms) classified `write`/ALLOW for builder/architect, including
while tainted.
### OH-6.5-embedded-placeholder (P1)
`findExecPayloadIsPlaceholderWipe` used exact `a === "{}"`, so
`find / -exec rm -rf {}/foo \;`, `{}foo`, `prefix{}` (and `-- {}/foo`)
classified `write`/ALLOW.
## Logic implemented (`src/guards/shell-parse.ts`)
1. **Added `import { posix } from "node:path";`** — the file previously had no
imports; POSIX normalization is stdlib-only.
2. **Added `normalizeWipeTarget(t)`** — fail-closed normalization: trims,
maps every `~`-leading form (`~`, `~/x`, `~<user>`, `~<user>/x` — unknown
home, fail closed) to `/`, expands `$HOME`/`${HOME}` to `/`, collapses
repeated slashes, then applies `posix.normalize`, so `//`, `///`, `/.`,
`/./`, `/../`, `/tmp/../` all resolve to `/` (matching `realpath -m` /
same device:inode as `/` per the audit).
3. **Rewrote `isRootWipeTarget(t)`** — preserves every pre-existing exact/prefix
rule (`*`, `/*`, `--no-preserve-root`, `~`, `~/*`, `~/…`, `$HOME`,
`${HOME}`, `$HOME/…`, `${HOME}/…`), adds `~<user>` fail-closed, and falls
through to `normalizeWipeTarget(t) === "/"`. Multi-line branch style is kept
so the wave-1 accept zero-stub scan (which flags `||` + `true` on one line)
stays clean.
4. **Added `isFindWipeRootOperand(p)`** — `true` when `isRootWipeTarget(p)` or
the operand contains a `..` segment (covers `..`, `../x`, `a/../b`),
i.e. the fail-closed workspace-escape approximation; `findHasWipeRoot`
now uses it, so `find ..`, `find //`, `find /tmp/../`, `find ~root` count
as wipe roots for the `-exec`-payload gate.
5. **Fixed the `-delete` branch** — was `args.filter(…).some(p === "/" || "/*")`
(exact match, and it wrongly swept `-exec` payload tokens into the path
set); now `findPathOperands(args).some(isFindWipeRootOperand)`, the same
normalized/outside-workspace check as the `-exec` branch.
6. **Embedded placeholder** — `a === "{}"` → `a.includes("{}")` in
`findExecPayloadIsPlaceholderWipe` (post-`--` tokens included, as before,
so `-- {}/foo` denies; `{}` never appears legitimately in `rm` targets).
7. **Taint mapping preserved** — `evaluateTaintGuard` already denies any
`isDestructiveShellCommand(command)` as `TAINT-DESTRUCTIVE-1` while tainted;
fixing the classifier automatically fixes the taint layer with no bypass.
Net `src/` delta: raw 11961 → 11981 (budget 12000), stripped 9961 → 9975
(budget 10000). Comments kept concise deliberately to stay under the
OH-scope-loc-budget hard caps pinned by
`test/audit-20261018-remediation.test.ts`.
## Tests added (`test/guards.test.ts`, +169 lines, 9 new tests, none modified)
- `OH-6.5-root-normalization` (5 tests): every probe row
(`rm -rf // /// /. /./ /../ /tmp/../ ~root ~root/.ssh`;
`find //|/./|/tmp/../|..|~root -exec rm -rf {} \;`;
`find //|/tmp/../|.. -delete`) asserts `class === "destructive"` +
`isDestructiveShellCommand === true` + `rule-1-destructive` deny for
builder/architect/director/qa-adversary; tainted sessions deny all 16 rows
via `evaluateTaintGuard` with `TAINT-DESTRUCTIVE-1`; scoped controls
(`rm -rf /tmp`, `/var/tmp/app`, `find /tmp … {}[ /{}/foo]`, `find . …`,
`find /tmp -name x -delete`) stay `write`/ALLOW.
- `OH-6.5-embedded-placeholder` (4 tests): `{}/foo`, `{}foo`, `prefix{}`,
`-- {}/foo` over `/` with `\;` and `-ok` terminators assert destructive +
`rule-1-destructive` for all four roles; tainted deny `TAINT-DESTRUCTIVE-1`;
scoped `find /tmp|… {}/foo` controls stay `write`/ALLOW.
## Gate results (exact commands + exit codes, from inside the worktree)
- `bun run check` (tsc --noEmit) → exit 0.
- `bun test test/guards.test.ts` (stock TMPDIR) → 41 pass, 0 fail, exit 0
(was 32 tests; +9 new).
- `bun test test/guards.test.ts test/audit-20261018-remediation.test.ts`
filtered `-t` re-runs of the two OS-temp tests (stock TMPDIR) → pass
(proves the TMPDIR-override failures below are environmental, not regressions).
- `TMPDIR=/dev/shm bun test` (full suite; override required because host
`/tmp` is inode-exhausted/ENOSPC) → 365 pass, 2 fail; both failures are the
audit-documented TMPDIR-flip tests (`redirect-escape` OS-temp allow,
`cp-mv-argv-targets` absolute-source allow) which pass with stock TMPDIR
and fail identically on the pristine tree (verified via `git stash` A/B).
The OH-scope-loc-budget hard-cap test passes (raw 11981 ≤ 12000).
- `TMPDIR=/dev/shm bash scripts/accept/wave-1.sh` → `WAVE 1 ACCEPT: ALL GATES
GREEN`, exit 0 (84 tests, zero-stub/scope scans clean).
- `TMPDIR=/dev/shm make accept` → wave 1 green; wave 2 fails ONLY on the two
pre-existing TMPDIR environmental tests above (identical with and without
this change, proven by stash A/B). Stock-TMPDIR accept runs cannot execute
on this host (`mkdtemp /tmp/*` → ENOSPC), as documented in the audit.
## Self-audit notes
- Fail-closed everywhere: unknown homes (`~user`), unresolvable normalizations
(catch → raw string, still matched), non-converging wrappers (untouched),
`..`-containing find roots, and embedded `{}` all resolve toward
`destructive`/deny, never allow.
- No stubs/mocks/vacuous tests: every new assertion checks the public guard
API (`classifyShellCommand`, `isDestructiveShellCommand`,
`evaluateBoardGuard`, `evaluateTaintGuard`) with distinct attack/allow
expectations; no `TODO`, `|| true`, empty catch, or `test.skip`.
- Out-of-scope findings untouched: OH-1.3/OH-1.1 (tags/publish), OH-6.6
(no upstream fork hook), OH-scope-loc-budget (budget observed, not revised),
OH-deps-caret/OH-1.1-no-gh-release (informational).
+45 -25
View File
@@ -62,6 +62,8 @@
* consumed as option values but never reported as file targets.
*/
import { posix } from "node:path";
export type ShellCommandClass = "read-only" | "write" | "destructive";
export interface ParsedShellCommand {
@@ -838,14 +840,21 @@ function findPathOperands(args: string[]): string[] {
/** Whether any leading find-path operand is a root-wipe target. */
function findHasWipeRoot(args: string[]): boolean {
return findPathOperands(args).some((p) => isRootWipeTarget(p));
return findPathOperands(args).some((p) => isFindWipeRootOperand(p));
}
/** Leading `find` path operand naming a wipe root (`//`, `/tmp/../`, `..`, `~root`). */
function isFindWipeRootOperand(operand: string): boolean {
const t = (operand ?? "").trim();
return t !== "" && (isRootWipeTarget(t) || t.replace(/\\/g, "/").split("/").includes(".."));
}
/**
* OH-6.5-find-exec-placeholder: `true` when an `-exec`/`-execdir`/`-ok`/
* `-okdir` payload runs `rm` with recursive+force flags over the `{}`
* placeholder (`rm -rf {}`). Callers gate this on a root-wipe find root so
* scoped uses (`find /tmp -name x -exec rm -rf {} \;`) stay write-class. A
* OH-6.5-find-exec-placeholder (+ OH-6.5-embedded-placeholder): `true` when
* an `-exec`/`-execdir`/`-ok`/`-okdir` payload runs `rm` with recursive+force
* flags over a `{}-including` target (`{}`, `{}/foo`, `{}foo`, `prefix{}` —
* substring match, fail-closed). Callers gate this on a wipe-root find root
* so scoped uses (`find /tmp -name x -exec rm -rf {} \;`) stay write-class. A
* payload substitution (`$(…)`/backticks) with recursive+force flags is
* destructive because a real shell expands it before `rm` sees its targets.
*/
@@ -893,7 +902,7 @@ function findExecPayloadIsPlaceholderWipe(payload: string): boolean {
if (!endOfOptions && a.startsWith("-") && a !== "-") {
continue;
}
if (a === "{}") {
if (a.includes("{}")) {
hasPlaceholder = true;
}
}
@@ -964,21 +973,34 @@ function splitChain(command: string): string[] {
.filter((part) => part.length > 0);
}
/** Fail-closed wipe-target normalization (`//`→`/`, `/tmp/../`→`/`, `~…`→`/`, `$HOME`→`/`). */
function normalizeWipeTarget(target: string): string {
const s = (target ?? "").trim();
if (s.startsWith("~")) return "/";
try {
return posix.normalize(s.replace(/\$\{HOME\}|\$HOME/g, "/").replace(/\/+/g, "/"));
} catch {
return s;
}
}
/** Targets whose recursive-force removal wipes a home/root scope. */
function isRootWipeTarget(target: string): boolean {
return (
target === "/" ||
target === "/*" ||
target === "*" ||
target === "--no-preserve-root" ||
target === "~" ||
target === "~/*" ||
target.startsWith("~/") ||
target === "$HOME" ||
target === "${HOME}" ||
target.startsWith("$HOME/") ||
target.startsWith("${HOME}/")
);
const t = (target ?? "").trim();
if (t === "*" || t === "/*" || t === "--no-preserve-root") {
return true;
}
if (t.startsWith("~")) {
return true; // `~`, `~/…`, `~<user>` fail closed
}
if (t === "$HOME" || t === "${HOME}") {
return true;
}
if (t.startsWith("$HOME/") || t.startsWith("${HOME}/")) {
return true;
}
// Normalized `/` covers `//`, `///`, `/.`, `/./`, `/../`, `/tmp/../`.
return normalizeWipeTarget(t) === "/";
}
/**
@@ -1135,13 +1157,11 @@ function segmentIsDestructive(segment: string): boolean {
}
if (exe === "find") {
// `find / -delete` removes the whole tree: destructive. Scoped deletes
// (`find /tmp/x -delete`) stay write-class via `segmentIsWrite`.
// `find / -delete` removes the whole tree: destructive, as do the
// normalized/escaping spellings (`//`, `/tmp/../`, `..`, `~root`).
// Scoped deletes (`find /tmp/x -delete`) stay write-class.
if (args.includes("-delete")) {
const pathArgs = args.filter(
(a) => !a.startsWith("-") && a !== ";" && a !== "+" && a !== "{}",
);
if (pathArgs.some((p) => p === "/" || p === "/*")) {
if (findPathOperands(args).some((p) => isFindWipeRootOperand(p))) {
return true;
}
}
+169
View File
@@ -20,6 +20,11 @@ import {
isDestructiveShellCommand,
isFileMutatingShellCommand,
} from "../src/guards/shell-parse";
import {
clearTaintState,
evaluateTaintGuard,
recordUntrustedContentTaint,
} from "../src/guards/taint-guard";
function verdict(request: GuardRequest) {
return evaluateBoardGuard(request, defaultGuardConfig());
@@ -379,3 +384,167 @@ describe("permission-guard staff roles (OH-8.3)", () => {
}
});
});
describe("OH-6.5-root-normalization: slash/dot-dot/tilde wipe-root spellings are destructive (P0)", () => {
const ATTACK_ROLES = ["builder", "architect", "director", "qa-adversary"];
test("rm -rf over normalized wipe roots is destructive for every role", () => {
for (const command of [
"rm -rf //",
"rm -rf ///",
"rm -rf /.",
"rm -rf /./",
"rm -rf /../",
"rm -rf /tmp/../",
"rm -rf ~root",
"rm -rf ~root/.ssh",
]) {
expect(classifyShellCommand(command).class).toBe("destructive");
expect(isDestructiveShellCommand(command)).toBe(true);
for (const role of ATTACK_ROLES) {
const result = verdict({ role, tool: "shell", command });
expect(result.allowed).toBe(false);
expect(result.rule).toBe("rule-1-destructive");
}
}
});
test("find -exec placeholder wipe over normalized/escaping roots is destructive for every role", () => {
for (const command of [
"find // -exec rm -rf {} \\;",
"find /./ -exec rm -rf {} \\;",
"find /tmp/../ -exec rm -rf {} \\;",
"find .. -exec rm -rf {} \\;",
"find ~root -exec rm -rf {} \\;",
]) {
expect(classifyShellCommand(command).class).toBe("destructive");
expect(isDestructiveShellCommand(command)).toBe(true);
for (const role of ATTACK_ROLES) {
const result = verdict({ role, tool: "shell", command });
expect(result.allowed).toBe(false);
expect(result.rule).toBe("rule-1-destructive");
}
}
});
test("find -delete over normalized/escaping roots is destructive for every role", () => {
for (const command of ["find // -delete", "find /tmp/../ -delete", "find .. -delete"]) {
expect(classifyShellCommand(command).class).toBe("destructive");
expect(isDestructiveShellCommand(command)).toBe(true);
for (const role of ATTACK_ROLES) {
const result = verdict({ role, tool: "shell", command });
expect(result.allowed).toBe(false);
expect(result.rule).toBe("rule-1-destructive");
}
}
});
test("tainted sessions deny every root-normalization probe at the taint layer", () => {
const session = `oh65-root-norm-taint-${Date.now()}`;
const commands = [
"rm -rf //",
"rm -rf ///",
"rm -rf /.",
"rm -rf /./",
"rm -rf /../",
"rm -rf /tmp/../",
"rm -rf ~root",
"rm -rf ~root/.ssh",
"find // -exec rm -rf {} \\;",
"find /./ -exec rm -rf {} \\;",
"find /tmp/../ -exec rm -rf {} \\;",
"find .. -exec rm -rf {} \\;",
"find ~root -exec rm -rf {} \\;",
"find // -delete",
"find /tmp/../ -delete",
"find .. -delete",
];
for (const command of commands) {
recordUntrustedContentTaint(session, "webfetch");
const tainted = evaluateTaintGuard(session, command, "shell");
expect(tainted.action).toBe("deny");
expect(tainted.ruleId).toBe("TAINT-DESTRUCTIVE-1");
clearTaintState(session);
}
});
test("scoped controls stay write-class and allowed for builder", () => {
for (const command of [
"rm -rf /tmp",
"rm -rf /tmp/wave1-abcd",
"rm -rf /var/tmp/app",
"find /tmp -name x -exec rm -rf {} \\;",
"find /tmp -name x -exec rm -rf {}/foo \\;",
"find . -exec rm -rf {} \\;",
"find . -exec rm -rf {}/foo \\;",
"find /tmp -name x -delete",
]) {
expect(classifyShellCommand(command).class).toBe("write");
expect(isDestructiveShellCommand(command)).toBe(false);
expect(verdict({ role: "builder", tool: "shell", command }).allowed).toBe(true);
}
});
});
describe("OH-6.5-embedded-placeholder: suffixed/embedded {} is a placeholder wipe (P1)", () => {
const ATTACK_ROLES = ["builder", "architect", "director", "qa-adversary"];
test("embedded {} payloads over / are destructive for every role", () => {
for (const command of [
"find / -exec rm -rf {}/foo \\;",
"find / -exec rm -rf {}foo \\;",
"find / -exec rm -rf prefix{} \\;",
"find / -exec rm -rf -- {}/foo \\;",
]) {
expect(classifyShellCommand(command).class).toBe("destructive");
expect(isDestructiveShellCommand(command)).toBe(true);
for (const role of ATTACK_ROLES) {
const result = verdict({ role, tool: "shell", command });
expect(result.allowed).toBe(false);
expect(result.rule).toBe("rule-1-destructive");
}
}
});
test("embedded {} also denies with -ok terminators", () => {
for (const command of [
"find / -ok rm -rf {}/foo \\;",
"find / -ok rm -rf {}foo \\;",
"find / -ok rm -rf prefix{} \\;",
]) {
expect(isDestructiveShellCommand(command)).toBe(true);
for (const role of ATTACK_ROLES) {
const result = verdict({ role, tool: "shell", command });
expect(result.allowed).toBe(false);
expect(result.rule).toBe("rule-1-destructive");
}
}
});
test("tainted sessions deny embedded {} wipes at the taint layer", () => {
const session = `oh65-embedded-taint-${Date.now()}`;
for (const command of [
"find / -exec rm -rf {}/foo \\;",
"find / -exec rm -rf {}foo \\;",
"find / -exec rm -rf prefix{} \\;",
"find / -exec rm -rf -- {}/foo \\;",
]) {
recordUntrustedContentTaint(session, "webfetch");
const tainted = evaluateTaintGuard(session, command, "shell");
expect(tainted.action).toBe("deny");
expect(tainted.ruleId).toBe("TAINT-DESTRUCTIVE-1");
clearTaintState(session);
}
});
test("scoped embedded-{} controls stay write-class and allowed", () => {
for (const command of [
"find /tmp -name x -exec rm -rf {}/foo \\;",
"find . -name x -exec rm -rf {}/foo \\;",
]) {
expect(classifyShellCommand(command).class).toBe("write");
expect(isDestructiveShellCommand(command)).toBe(false);
expect(verdict({ role: "builder", tool: "shell", command }).allowed).toBe(true);
}
});
});