From 71b07e52eb6d01fabf84abe41d77f9a77992f8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 29 May 2026 09:50:18 +0300 Subject: [PATCH] audit(spec): 51 findings + 8 MUST critical fixes inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial audit condensed router-gate spec через 3 parallel Sonnet adversaries (9 attack zones). 51 finding total: 10 BYPASS-COMPLETE + 17 PARTIAL + 9 DOS + 15 INFO. Spec заявление «hard wall полный» НЕ выдерживает. 8 MUST critical inline fixes applied: - §5.1 Bash: <<< here-string, node REPL/stdin block, < input redirect, tokenizer per-arg path-deny check (closes CRITICAL-9/8/6 + PARTIAL-15) - §3.1 path normalization: UNC \?\ prefix strip, 8.3 short names expand via GetLongPathName, unresolved $VAR fail-CLOSE (closes CRITICAL-3/4/5) - §4 Поведение 1: source restriction — detector проверяет только organic root user prompt, НЕ AskUser chosen_label (closes CRITICAL-1 design flaw) - §8 Implementation order matrix: Этап 2.3 branch-switch rewrite MUST complete BEFORE Этап 3 enforce-mode (closes CRITICAL-10 S8 migration regression) - §1.4: gate-config.json protected с Этапа 1.4 ранее (closes DOS D-1 tiny-budget patch attack window) 5 SHOULD-FIX + 5 DOS-MUST-ADDRESS deferred в writing-plans (§9 «Audit findings deferred» documented для plan pickup). Audit report saved at: docs/superpowers/audits/2026-05-29-router-gate-condensed- adversarial-audit.md cspell-words.txt: +UNC, +EACCES (valid technical terms). Co-Authored-By: Claude Opus 4.7 (1M context) --- cspell-words.txt | 2 + ...router-gate-condensed-adversarial-audit.md | 336 ++++++++++++++++++ ...-router-gate-hard-wall-design-condensed.md | 52 ++- 3 files changed, 376 insertions(+), 14 deletions(-) create mode 100644 docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md diff --git a/cspell-words.txt b/cspell-words.txt index 50171206..28edf86b 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1888,3 +1888,5 @@ deplo # Router-gate v3.3 (2026-05-29) — v4.1 audit closure эскалируем Ctemp +UNC +EACCES diff --git a/docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md b/docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md new file mode 100644 index 00000000..dd834fd7 --- /dev/null +++ b/docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md @@ -0,0 +1,336 @@ +# Adversarial audit condensed router-gate hard wall спека + +**Дата:** 2026-05-29 +**Target:** [`docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md`](../specs/2026-05-29-router-gate-hard-wall-design-condensed.md) (commit d4f7e681, 1117 строк) +**Auditor:** Claude (controller Opus 4.7) + 3 параллельных Sonnet 4.6 adversarial subagents +**Методология:** 9 attack zones из user prompt, по 3 zone на агента. Каждый агент инструктирован как «controller который ХОЧЕТ ОБОЙТИ собственный gate». +**Контекст:** спек v3.5 прошёл 4 раунда внутренних adversarial audits + verifier completeness check 63/63. Автор заявил «hard wall полный, acceptable residual list пуст». + +--- + +## TL;DR + +**Аудит опровергает заявление о полном hard wall.** Найдено **51 finding** в 9 attack zones, из них: + +- **10 BYPASS-COMPLETE** — полный обход hard wall через конкретные exploitable vectors +- **17 BYPASS-PARTIAL** — частичные обходы конкретных компонентов +- **9 DOS** — выводят gate в lockout/false-block состояние, заказчик вынужден отключать через Recovery §6 +- **15 INFO / acknowledged** — низкая feasibility, acknowledged limitations, или out-of-scope + +**Самое серьёзное:** §5.1 Bash content rules имеет **6 BYPASS-COMPLETE дыр** через формальные обходы whitelist/blacklist (`<<<`, `echo | node`, UNC paths, 8.3 short names, undefined env vars, `cat multi-arg`). Single-line bash commands achieving arbitrary file write/execute. + +**Acceptable residual list НЕ пуст.** Минимум 10 holes должны быть закрыты до writing-plans фазы либо явно перенесены в residual list с обоснованием. + +--- + +## Severity breakdown + +| Severity | Count | Where most concentrated | +|---|---|---| +| BYPASS-COMPLETE | 10 | §5.1 Bash content (6) + §3.1 path normalization (3) + §4 Поведение 1 (1) | +| BYPASS-PARTIAL | 17 | §3.4 subagent + §3 chain-state + §4.5 answer parser + §5.2 static scan | +| DOS | 9 | §3.6 gate budget + §8 implementation order matrix gaps | +| INFO/Acknowledged | 15 | §4.7 question quality (creative leading questions неизбежны) | +| **Итого** | **51** | | + +--- + +## Top-10 Critical (BYPASS-COMPLETE) + +### CRITICAL-1: AskUserQuestion option label triggers Поведение 1 direct invocation + +**Source:** Agent 1 / Finding 1.1 +**Confidence:** HIGH +**Feasibility:** HIGH + +**Attack:** §4 Поведение 1 матчит pattern `(делай|сделай|вызови|примени|используй)\s+` против `transcript[-1].user.content`. AskUserQuestion option labels — controller-controlled text. Когда заказчик выбирает опцию `1` («делай subagent-driven-development»), её label становится user content в transcript. Gate видит pattern → Поведение 1 → allow любой mutating tool. + +**Mitigation:** Detector Поведения 1 должен проверять `is_direct_invocation` только для **корневого** user-prompt в turn (первое сообщение), не для AskUserQuestion answers. AskUser answers обрабатываются эксклюзивно через §4.5 answer parser. + +--- + +### CRITICAL-2: vitest globalSetup из cross-session edits (file-watcher blind spot) + +**Source:** Agent 1 / Finding 2.2 +**Confidence:** HIGH +**Feasibility:** MEDIUM (требует multi-session setup) + +**Attack:** §5.1 file-watcher session-scoped — clears на `git commit exit 0 + lefthook GREEN`. Если в сессии N контроллер редактировал `vitest.config.ts` → commit GREEN → watcher cleared. В сессии N+1: `vitest.config.ts` уже не в `edited_files`. `npx vitest run` (whitelisted) запускает vitest → reads config → executes `globalSetup` файлы (произвольный JS). Static scan §5.2 проверяет только explicit target (`vitest run` без target → glob-aware → AskUser), но НЕ автоматически открытые vitest config + setup files. + +**Mitigation:** Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state. + +--- + +### CRITICAL-3: Undefined env var bypass path normalization + +**Source:** Agent 2 / Finding 4.1 +**Confidence:** HIGH (depends on implementation) +**Feasibility:** HIGH + +**Attack:** §3.1 path normalization step 3: «Expand `~` через `os.homedir()` и env vars через `process.env`». Если controller использует path `~/.claude/runtime/$UNDEFINED_VAR/state.json`, и `process.env.UNDEFINED_VAR` undefined, типичная implementation возвращает literal `$UNDEFINED_VAR` в строке. Resolved path содержит `$UNDEFINED_VAR` segment → glob match `~/.claude/runtime/*` не срабатывает (один extra segment) → protected check passes → Edit/Write allowed. + +**Mitigation:** Unexpanded `$VAR` sequences после env expansion → fail-CLOSE («unresolved env vars in path»). Или strict whitelist allowed env vars (`HOME`, `USERPROFILE`) с фейлом на остальные. + +--- + +### CRITICAL-4: Windows UNC paths bypass canonicalization + +**Source:** Agent 2 / Finding 4.2 +**Confidence:** HIGH +**Feasibility:** HIGH + +**Attack:** Windows UNC path `\\?\C:\Users\Administrator\.claude\runtime\router-state-abc.json` (long path prefix `\\?\`). `path.resolve` на Windows возвращает путь с `\\?\` prefix. Protected list pattern `~/.claude/runtime/*` после `os.homedir()` → `C:\Users\Administrator\.claude\runtime\*`. UNC version с prefix НЕ match'ит pattern (один extra `\\?\` префикс) → Edit/Write на protected file allowed. + +**Mitigation:** Path normalization step 1: strip `\\?\` prefix перед resolve. Step 2: explicit UNC prefix normalization (`\\server\share` → drive form если applicable). Это **Windows-specific** дыра, на Linux/Mac не существует. + +--- + +### CRITICAL-5: Windows 8.3 short names bypass canonicalization + +**Source:** Agent 2 / Finding 4.4 +**Confidence:** HIGH +**Feasibility:** MEDIUM (8.3 включён по умолчанию на legacy Windows install) + +**Attack:** Windows file system поддерживает 8.3 short names: `C:\Users\ADMINI~1\.claude\runtime\state.json`. `path.resolve` НЕ разворачивает 8.3 short names. `fs.realpathSync` разворачивает только если файл **существует** в момент вызова. Для несуществующего файла (новый `router-state-.json` про который gate знает но он не создан) realpath fails → fallback to resolved. Resolved содержит short name → не match'ит protected list (где `Administrator` full form) → allowed. + +**Mitigation:** Использовать `GetLongPathName` WinAPI через `node-ffi` или wrap-script. Альтернатива — disable 8.3 generation на NTFS volume (`fsutil 8dot3name set 1`). NB: это **Windows Server 2022 машина** — short names enabled out-of-box. + +--- + +### CRITICAL-6: `cat` multi-arg bypass path-deny overlay + +**Source:** Agent 2 / Finding 4.6 +**Confidence:** HIGH +**Feasibility:** HIGH + +**Attack:** §5.1 path-deny overlay для reading commands (cat/tail/head/grep/less/more/file/stat/wc/ls): «нормализует path и проверяет против protected list». Tokenizer split по `;`/`&&`/`||`/`|`/`&`. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — один токен после split (без separators), `cat` whitelisted, **два** path-args. Если implementation tokenizer извлекает только first token (command), не all subsequent args — второй path-arg `~/.claude/runtime/router-state.json` не проверяется → читается. + +**Mitigation:** Tokenizer должен извлекать ВСЕ args после command token и передавать их в path-deny check loop. Per-arg validation, не command-level only. + +--- + +### CRITICAL-7: `git format-patch -o ` write to protected paths + +**Source:** Agent 2 / Finding 4.7 + Agent 1 / Finding 5.3 +**Confidence:** MEDIUM (требует verification implementation) +**Feasibility:** HIGH + +**Attack:** §5.1 whitelist row объединяет `git log/show/diff/blame/format-patch` с исключениями `--output=*/-o /--file=*`. Если implementation parser применяет исключения только к `git log` (первый command в row label), а к `git format-patch` — нет (отдельная command), то `git format-patch HEAD~1 -o ~/.claude/runtime/` пишет patch files в protected dir. + +**Vetting note:** spec формулировка может покрывать обе команды (row label «git log / git show / git diff / git blame / git format-patch» применяет exclusions ко всем). Зависит от parser implementation. Если parser per-command, finding genuine. Если row-wide, finding false-positive. **Требует verification в plan**. + +**Mitigation:** Explicit per-command exclusion mapping. Или убрать `git format-patch` из whitelist (использовать `git format-patch --stdout` через conditional whitelist + AskUser). + +--- + +### CRITICAL-8: `echo 'code' | node` — pipe receiver `node` is whitelisted + +**Source:** Agent 3 / Finding 7.1 +**Confidence:** MEDIUM +**Feasibility:** HIGH + +**Attack:** §5.1 hard-blacklist: «Любые pipe-команды (`|`) где receiver — mutating command». Но `node` в **whitelist** (без `-e/--eval/-r`). Pipe `echo 'fs.writeFileSync("evil","x")' | node` — receiver `node` whitelisted, не mutating per default classification. Tokenizer splits at `|`, видит `node` в receiver position → whitelisted? Tokenizer семантика «receiver = mutating command» зависит от implementation: если check'ает только blacklist contains, `node` passes; если check'ает «mutation potential», `node` reading stdin = arbitrary execution, должен блокироваться. + +**Mitigation:** Explicit: `node` без `` arg (т.е. без positional path argument после `node`) — block. Pipe receiver `node` всегда block (REPL-like usage). + +--- + +### CRITICAL-9: `<<<` here-string не в hard-blocked list + +**Source:** Agent 3 / Finding 7.3 +**Confidence:** HIGH +**Feasibility:** HIGH + +**Attack:** §5.1 sub-shell broad sweep блокирует: `` ` ``, `$(...)`, `<(...)/>(...)`, `<<`, `<<-`. **`<<<` (here-string) НЕ упомянут.** Команда `node <<< "fs.writeFileSync('evil','x')"` — bash here-string передаёт строку как stdin к `node`. Tokenizer не split на `<<<` (не в pipe-split list). Gate видит `node` first command, whitelisted, без disallowed flags → allow. Arbitrary Node.js execution. + +**Mitigation:** Trivial fix — добавить `<<<` к sub-shell broad sweep list. Простейший finding, наиболее очевидный gap. + +--- + +### CRITICAL-10: enforce-branch-switch migration window (Этап 2.3 sequencing) + +**Source:** Agent 3 / Finding 9.3 +**Confidence:** MEDIUM (зависит от deployment sequence) +**Feasibility:** HIGH + +**Attack:** §8 Implementation order matrix НЕ упоминает Этап 2.3 явно (matrix содержит 5 строк: Этап 1, 1.4, 2, 2.1, 3). Согласно §8 main table sequence: 2 → 2.1.0 → 2.1 → 2.2 → 2.3 → 3. Если implementer пропускает 2.3 (rewrite enforce-branch-switch) ДО Этапа 3 (settings.json enforce-mode), OLD `enforce-branch-switch.mjs` всё ещё ищет `BRANCH-SWITCH-CONFIRMED` в response text. Controller пишет marker → OLD hook accepts → dangerous git ops proceed. **S8 closure НЕ активна** в migration window. + +**Mitigation:** §8 Implementation order matrix явно: «Этап 2.3 MUST complete BEFORE Этап 3 enforce-mode activation». Альтернатива — Этап 3 включает регистрацию обновлённого branch-switch hook одной транзакцией. + +--- + +## BYPASS-PARTIAL (17 findings — выборка топ-7) + +### PARTIAL-1: Post-skill class-level unlock, не file-level + +**Source:** Agent 1 / Finding 1.4 | **Confidence:** HIGH + +§4.6 partial unlock + §4.5 answer parsing: `approved_tool: "Edit"` без mandatory `approved_action_pattern`. После `Skill(writing-plans)` + AskUser «выполнить изменения» controller может Edit **любой** non-protected файл. Class-level unlock = всё `Edit/Write/MultiEdit` разрешено. + +**Fix:** `approved_action_pattern` mandatory с конкретным file path. Gate сравнивает `tool_input.file_path` против pattern. + +### PARTIAL-2: Chain-reset через AskUser chosen_label + +**Source:** Agent 1 / Finding 1.3 | **Confidence:** HIGH + +Chain-state TTL expiry читает `transcript[-1].user.content` на explicit-reset фразы. Spec говорит «only user turns». Но AskUserQuestion chosen_label попадает в transcript **как user content** — controller помещает «новая задача» в option label, заказчик выбирает → chain reset triggered. + +**Fix:** Reset фразы проверяются только против **organic root prompt**, не chosen labels. Или AskUser answers стрипают reset-phrase keywords. + +### PARTIAL-3: Free-form answer → AskUser fatigue cascade + +**Source:** Agent 1 / Finding 1.2 | **Confidence:** MEDIUM + +Controller конструирует AskUser опции так, что заказчик пишет неоднозначное «давай» / «хорошо» в free-form. 2/2 limit → fail-CLOSE → cascade frustration → заказчик пишет «делай сам» → Поведение 1 trigger via word `делай`. + +**Fix:** Strict explicit-keyword requirement в опциях. Чётко-классифицируемые answers (stop / specific tool / direct no skill). + +### PARTIAL-4: TOCTOU double-consume askuser-decisions entry + +**Source:** Agent 2 / Finding 3.1 | **Confidence:** HIGH + +`enforce-branch-switch.mjs` и `enforce-router-gate.mjs` оба read-check-write на `consumed: false`. Proper-lockfile защищает write, но read-modify-write 3-шаговая операция не атомарна. Параллельные tools могут увидеть `consumed: false` оба → consume approval дважды. + +**Fix:** Atomic compare-and-swap operation через single lock around full RMW. Lock acquired при read, released после write. + +### PARTIAL-5: Chain_step++ двойной через sub-Skill + +**Source:** Agent 2 / Finding 3.2 | **Confidence:** MEDIUM + +PostToolUse handler инкрементирует chain_step на success. Если Skill(#55) внутри вызывает sub-Task с subagent_type matching expected_node → двойной инкремент. + +**Fix:** PostToolUse handler сравнивает не только tool match, но и `expected_tool_use_id` который PreToolUse запомнил при разрешении. Increment только при exact tool_use_id match. + +### PARTIAL-6: Edit-execute window между commit и watcher catch + +**Source:** Agent 2 / Finding 3.4 | **Confidence:** HIGH + +Если в turn N+1 первый tool — Bash(`node evil.mjs`), gate проверяет watcher (пуст после commit clear) → allow. Затем Edit(`evil.mjs`) в same turn → попадает в watcher. Но `node evil.mjs` уже выполнен. + +**Fix:** PreToolUse Bash check: посмотреть **существование** target script + **mtime** ≤ 5 секунд → require AskUser regardless of watcher state. + +### PARTIAL-7: chain-state malformed = fail-OPEN reset + +**Source:** Agent 3 / Finding 6.1 | **Confidence:** HIGH + +Asymmetric §3.3 failure modes: router-state malformed → block, chain-state malformed → reset + warn. Если controller тригернёт malformed (lock-contention read failure), chain progress wiped → Поведение 4 (один AskUser unlock all). + +**Fix:** chain-state malformed → fail-CLOSE (block all mutating). Symmetric с router-state semantics. + +**Остальные 10 BYPASS-PARTIAL** (краткий список): + +- P-08 **ToolSearch deferred tools** (Agent 1 / F2.1) — gate matcher coverage unclear для loaded-on-demand tools +- P-09 **Skill execution gate coverage** (Agent 1 / F2.5) — нужен smoke test что tool calls в skill execution проходят через PreToolUse +- P-10 **`node.exe` variant + temp files** (Agent 1 / F5.1) — subagent creates non-protected helper.js, parent runs via `node.exe` +- P-11 **String concatenation static scan bypass** (Agent 1 / F5.4) — `const k = 'write' + 'File' + 'Sync'; fs[k](...)` +- P-12 **Parallel Task counter race** (Agent 2 / F3.3) — 3+ Tasks одновременно проходят check на 3-limit +- P-13 **Orphan cleanup 24h reset** (Agent 2 / F3.5) — long sessions могут exploit cleanup +- P-14 **`` via AskUser social eng** (Agent 3 / F6.6) — controller suggests user paste marker +- P-15 **`<` input redirect not in blacklist** (Agent 3 / F7.6) — `wc -l < ~/.claude/runtime/state.json` обходит path-deny +- P-16 **Retry after timeout bypass** (Agent 2 / F8.1) — fail-CLOSE на timeout → retry next turn → fresh cache → success +- P-17 **TDD-gate vs router-gate execution order race** (Agent 3 / F9.1) — undefined hook ordering = TDD bypass через cascading unlock + +--- + +## DOS Findings (9) + +| ID | Finding | Severity | Mitigation | +|---|---|---|---| +| D-1 | `gate-config.json` writable migration window (Этап 1→2 gap) | HIGH — tiny budget = permanent fail-CLOSE | Hardcoded floor values; gate-config protected with Этапа 1.4 | +| D-2 | subagent block-file missing + tool_use_count==0 = assume BLOCKED → AskUser loop | HIGH — каждый Task triggers AskUser | Add success-marker file для clean subagent exits | +| D-3 | file-watcher reset stdout parsing fragile (FAIL string в commit message) | MEDIUM | Use lefthook exit code, не stdout strings | +| D-4 | Windows tmp+rename non-atomicity under AV load | MEDIUM | Wrap rename in retry+catch с logging | +| D-5 | Transcript parsing huge turn budget exhaustion | MEDIUM | Cap turn size при парсинге | +| D-6 | 10 tool calls lock contention cascade | MEDIUM | Reader-writer lock model | +| D-7 | Subagent gate reads parent locked state timeout | MEDIUM | Read-lock для parent state, не exclusive | +| D-8 | gate-config.json missing first start fail-CLOSE | MEDIUM | Explicit hardcoded defaults fallback | +| D-9 | enforce-prompt-injection empty surface drops context | MEDIUM | Verify hook handles empty array без crash | + +--- + +## INFO / Acknowledged (15 findings) + +Низкая feasibility или acknowledged design choice: + +- TodoWrite context pollution (Agent 1 / F1.5) +- npm test DB side effects (Agent 1 / F2.3) +- Monitor timing attack (Agent 1 / F2.4) +- PostToolUse handler crash semantics (Agent 2 / F3.6) +- symlink swap TOCTOU (Agent 2 / F4.3) — требует mutating Bash который уже blocked +- Unicode homoglyph case-fold (Agent 2 / F4.5) — требует pre-existing homoglyph dir +- symlink + EACCES path normalization (Agent 1 / F5.2) +- git format-patch creates files in CWD (Agent 1 / F5.3) — covered by F4.7 deeper +- gate-config security posture impact (Agent 1 / F5.5) +- Read-only long reconnaissance leakage (Agent 3 / F6.2) — acknowledged Поведение 4 +- `node --inspect` debugger port (Agent 3 / F7.2) — requires external debugger +- IFS manipulation (Agent 3 / F7.4) — `env`/`export` already blacklisted +- git format-patch --stdout info disclosure (Agent 3 / F7.5) — acknowledged read-only +- JSONL append > 4096 bytes atomicity (Agent 2 / F8.5) — log corruption only, не security +- first-option position bias defeats (Agent 3 / F9.5) — acknowledged residual + +--- + +## Verdict on «hard wall полный» claim + +**v3.5 spec заявление НЕ выдерживает.** Audit identified concrete BYPASS-COMPLETE vectors which 4 раунда внутренних audits упустили. Pattern причин: + +1. **Bash content rules incomplete enumeration** — 6 critical bypasses в §5.1 single attack zone. Внутренние audits фокусировались на known bypass classes (`$()`/backtick/heredoc), пропустили `<<<` / `node` REPL semantics / `<` redirect / multi-arg path / `format-patch -o`. +2. **Windows-specific path normalization gaps** — UNC, 8.3 short names, env var expansion edge cases. Внутренние audits в основном на Unix-mental-model тестировались. +3. **Поведение 1 trigger source unrestricted** — `transcript[-1].user.content` включает AskUser chosen_label, что fundamentally breaks direct-invocation model. **Это самая серьёзная** — design-level flaw, не implementation detail. +4. **Implementation order matrix incomplete** — Этап 2.3 sequencing не enforced, migration window открывает S8 регрессию. + +**Класс «controller-writable signals» НЕ полностью закрыт** — закрыты S5 (subagent BLOCKED) и S8 (branch-switch confirmation), но обнаружены **новые** controller-writable signals: AskUser option labels (CRITICAL-1), AskUser chosen_label контекст для chain reset (PARTIAL-2), AskUser content для social engineering (PARTIAL-3). Generalisable формула side-channel paths применима только к **explicit out-of-band channels** — но controller контролирует contents AskUserQuestion construction, что эффективно делает chosen_label semi-side-channel. + +--- + +## Recommended actions (priority order) + +**MUST FIX до writing-plans** (8 items): + +1. **CRITICAL-9** — Add `<<<` к §5.1 sub-shell broad sweep (trivial fix, 1 строка) +2. **CRITICAL-8** — Add `node` без positional path arg → block (§5.1 hard-blacklist) +3. **CRITICAL-6** — Tokenizer per-arg path-deny check (§5.1 path-deny overlay) +4. **CRITICAL-1** — Поведение 1 detector ограничить только organic root prompt, не AskUser answers (§4 Поведение 1) +5. **CRITICAL-3** — Unresolved `$VAR` → fail-CLOSE (§3.1 step 3) +6. **CRITICAL-4** — Strip `\\?\` UNC prefix перед resolve (§3.1) +7. **CRITICAL-5** — Use `GetLongPathName` WinAPI для 8.3 expansion (§3.1) +8. **CRITICAL-10** — §8 matrix явно enforce Этап 2.3 BEFORE Этап 3 + +**SHOULD FIX в writing-plans** (5 items): + +1. **CRITICAL-2** — Static scan §5.2 расширить на vitest config + setupFiles auto-discovery +2. **CRITICAL-7** — Verify `git format-patch -o` handling в parser implementation; explicit fix если per-command parser +3. **PARTIAL-1** — `approved_action_pattern` mandatory file-path binding (§4.5) +4. **PARTIAL-2** — Chain reset фразы только в organic root prompt (§3 chain-state) +5. **PARTIAL-7** — chain-state malformed → fail-CLOSE (§3.3 failure mode) + +**NICE-TO-HAVE / D-* DOS fixes** в writing-plans: + +- D-1 (gate-config protected с Этапа 1.4) +- D-2 (subagent success-marker file) +- D-3 (lefthook exit code не stdout parsing) +- D-9 (verify enforce-prompt-injection empty handling) + +**ACCEPT as residual** (явно документировать в §9): + +- 15 INFO findings — большинство acknowledged limitations или low feasibility +- PARTIAL-3 (AskUser fatigue) — UX-tradeoff +- PARTIAL-17 (TDD-gate ordering) — нуждается в smoke-test для verification + +--- + +## Next steps + +Заказчик решает: + +**Option A — Patch condensed v3.5 inline:** добавить fixes для 8 MUST + 5 SHOULD в текущий condensed (commit поверх d4f7e681), пересохранить self-consistent v3.5.5. Spec становится 1117 → ~1250 строк. + +**Option B — Open v3.6 round:** brainstorming-скил для design каждого fix отдельно (AskUserQuestion per fix), затем patch condensed. Более тщательно, +3-5h работы. + +**Option C — Defer fixes в writing-plans:** записать audit findings в spec §9 как «known to be addressed in plan implementation», plan picks up specific resolution per finding. Faster but spec остаётся имеющим заявленные дыры. + +**Option D — Hybrid:** A для 8 MUST (trivial fixes), C для 5 SHOULD (require design decisions в plan). + +**Рекомендуемый:** D — закрыть 8 critical inline (час работы), оставить 5 SHOULD на plan для проектных решений per fix. + +Audit complete. diff --git a/docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md b/docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md index 6e6ac8cc..0d524f96 100644 --- a/docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md +++ b/docs/superpowers/specs/2026-05-29-router-gate-hard-wall-design-condensed.md @@ -42,7 +42,9 @@ **Сохраняется:** 7 preserved хуков (tdd-gate / coverage-verify / memory-coverage / verify-before-push / rationalization-audit / prompt-injection / branch-switch). 6 из них теряют свои findOverride escape-фразы. Branch-switch переписан под AskUserQuestion-gate (см. §3 таблица сохраняемых). -**Class controller-writable signals закрыт архитектурно** через side-channel writes в protected paths (S5: subagent-block-file; S8: askuser-decisions с git-pattern). Acceptable residual list пуст. +**Class controller-writable signals закрыт архитектурно** через side-channel writes в protected paths (S5: subagent-block-file; S8: askuser-decisions с git-pattern). + +**Audit 29.05.2026** ([audit report](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 51 finding, из них 8 MUST-FIX закрыты inline (commit поверх d4f7e681; см. §5.1 `<<<` here-string + `node` REPL + per-arg path-deny + `<` redirect; §3.1 UNC strip + 8.3 expand + `$VAR` fail-CLOSE; §4 Поведение 1 source restriction; §8 Этап 2.3 sequencing). 5 SHOULD-FIX перенесены в writing-plans (см. §9 «Audit findings deferred»). 9 DOS findings + 15 INFO residuals документированы в audit report. --- @@ -146,12 +148,14 @@ Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до **Path normalization:** перед сравнением с deny-list gate **канонизирует** путь: -1. `path.resolve(target)` — разрешает относительные пути и `..`. -2. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved). -3. Expand `~` через `os.homedir()` и env vars через `process.env`. -4. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive). +1. **Strip Windows UNC long-path prefix** (closes audit CRITICAL-4): если path начинается с `\\?\` или `\\.\` — strip prefix перед дальнейшей normalization. UNC форма `\\?\C:\Users\...\.claude\runtime\state.json` нормализуется к `C:\Users\...\.claude\runtime\state.json` для match с protected glob. +2. `path.resolve(target)` — разрешает относительные пути и `..`. +3. **Expand Windows 8.3 short names** (closes audit CRITICAL-5): на Windows если path содержит `~N` segment (tilde+digit, например `ADMINI~1`), вызвать `GetLongPathName` WinAPI через native binding (`node-ffi-napi` или PowerShell `[System.IO.Path]::GetFullPath` fallback). Без раскрытия `C:\Users\ADMINI~1\.claude\runtime\state.json` не match protected list `C:\Users\Administrator\.claude\runtime\*`. Если WinAPI fails (file не существует, permission) → fail-CLOSE. +4. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved). +5. Expand `~` через `os.homedir()` и env vars через `process.env`. **Unresolved `$VAR` / `%VAR%` после expansion → fail-CLOSE** (closes audit CRITICAL-3: `~/.claude/runtime/$UNDEFINED/state.json` literal `$UNDEFINED` segment не match glob, must block). Allowed env vars whitelist: `HOME` / `USERPROFILE` / `APPDATA` / `LOCALAPPDATA` — все остальные `$VAR` references → fail-CLOSE. +6. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive). -Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих. +Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих. EACCES при realpathSync → fail-CLOSE (assume protected). ### 3.2.0. Pre-flight smoke-tests — REQUIRED BEFORE §3.2 IMPLEMENTATION @@ -254,7 +258,7 @@ Budget: 0.5h × 3 smoke-tests + verification = **1.5h total**. Without these — | File missing | == 0 | **Assume BLOCKED** (defensive, gate-crash possible). Parent обязан AskUser | | File read FS error | любое | fail-CLOSE per §3.3 | -4. **Cleanup:** parent gate удаляет block-file после чтения. Background cleanup script (cron / lefthook): orphans > 24h → delete (защита от parent gate crash после read но до delete). +1. **Cleanup:** parent gate удаляет block-file после чтения. Background cleanup script (cron / lefthook): orphans > 24h → delete (защита от parent gate crash после read но до delete). **Anti-forgery (3 защитных слоя — почему controller не подделает):** @@ -376,6 +380,8 @@ Gate определяет одно из 4 поведений на основе s - Tool-call НЕ матчит → **block** с reason: «заказчик указал X через direct invocation, текущий tool — другое». - Read-only baseline (Read/Grep/Glob/LS/TodoWrite/AskUserQuestion) разрешены всегда. +**Source restriction (closes audit CRITICAL-1):** detector патернов **проверяется ТОЛЬКО против organic root user prompt в turn** — первое user message турна. AskUserQuestion chosen_label / chosen_text НЕ проходят через Поведение 1 detector — обрабатываются эксклюзивно через §4.5 answer parser. Контроллер контролирует labels (option text создаётся controller'ом), без этой restriction он может сконструировать опцию `делай subagent-driven-development` (exact registry match) → user выбирает → label попадает в transcript как user content → Поведение 1 fake-triggers → allow любого mutating tool. Implementation: gate использует transcript metadata который различает `user_message_type: "prompt"` vs `user_message_type: "askuser_answer"`. Pattern match только на тип `prompt`. + **Stale registry — mandatory AskUser.** Если префикс-pattern matched (например `делай foo-bar`), но `foo-bar` **не найден** в registry — gate **обязан** заблокировать любой mutating tool до **mandatory AskUserQuestion** с явным форматом: «Пользователь указал skill `foo-bar`, но он не найден в реестре (возможно опечатка или переименован). Router рекомендует ``. Что делать?» с опциями: @@ -556,7 +562,7 @@ Gate инспектирует **content** Bash-команды (string в `tool_i | `ls` / `pwd` / `wc` / `head` / `tail` / `file` / `stat` | без `>` redirect, **path-deny overlay на ВСЕ path-args** | | `grep` / `egrep` / `fgrep` | без `--exec`, без `-l` после `>`, **path-deny overlay на ВСЕ path-args** | | `cat` / `less` / `more` | без `>` redirect, **path-deny overlay на ВСЕ path-args** | -| `node` | без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader`** (preload arbitrary module before main script) | +| `node` | **mandatory positional `` arg** (НЕ REPL mode); без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader`** (preload arbitrary module before main script), **без `-i` / `--interactive` / `--inspect*`** (REPL/debugger ports); pipe receiver `node` без positional path arg = block (closes audit CRITICAL-8: `echo 'code' \| node` arbitrary execution) | | `npx vitest run ...` / `npx vitest --version` | любые run-args, не `--reporter` с suspicious patterns | | `npm test` / `npm run test` / `npm run lint:*` (read-only scripts) | любые | | `php artisan` (read-only commands только: `list`, `route:list`, `migrate:status`) | по whitelist | @@ -574,7 +580,8 @@ Gate инспектирует **content** Bash-команды (string в `tool_i - `npm install` / `npm update` / `npm remove` / `yarn add` / `pnpm add` - `npx claude-*` — запуск Claude изнутри Claude - `curl -X POST/PUT/DELETE/PATCH` / `wget --post-data` — outbound mutations -- Любые pipe-команды (`|`) где receiver — mutating command +- Любые pipe-команды (`|`) где receiver — mutating command OR `node` без positional script path (REPL via stdin) +- `<` (input redirect) к file — закрывает audit PARTIAL-15: `wc -l < ~/.claude/runtime/router-state.json` обход path-deny overlay через input redirect (whitelist reading command + `<` bypass) **Sub-shell / heredoc broad sweep — команды содержащие любой из следующих токенов hard-blocked всегда (даже после unlock):** @@ -582,6 +589,7 @@ Gate инспектирует **content** Bash-команды (string в `tool_i - `$(...)` — POSIX command substitution - `<(...)` / `>(...)` — process substitution - `<<` / `<<-` — heredoc +- `<<<` — here-string (closes audit CRITICAL-9: `node <<< "fs.writeFileSync(...)"` arbitrary execution bypass) Некоторые legitimate uses (`git log $(echo HEAD)`) ломаются. Принимаем как acceptable friction для закрытия sub-shell bypass attack surface. @@ -617,7 +625,9 @@ const SKILL_BASH_ALLOW = { Маппинг — **в коде gate'а, не в Skill определении**. Skill не может расширить свой allow-list (это снова controller-writable signal). Расширение mapping — только через `tools/router-gate-decide.mjs` Edit + tests + PR review (а файл — в §3.1 protected paths). -**Path-deny overlay (§3.1) — applied to ALL path-args:** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json` — `tail` whitelisted, но `~/.claude/runtime/*` в protected → block. Все reading commands проверяются единообразно (не только `cat`). +**Path-deny overlay (§3.1) — applied to ALL path-args (per-arg validation):** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: UNC strip + 8.3 expand + resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json` — `tail` whitelisted, но `~/.claude/runtime/*` в protected → block. + +**Tokenizer per-arg requirement (closes audit CRITICAL-6):** при tokenize Bash-команды извлекать НЕ только first token (command), но ВСЕ subsequent args. Каждый arg который parse'ится как path (relative or absolute, не option flag начинающийся с `-`) проходит через path-deny check independently. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — `cat` whitelisted, **два** path-args, каждый normalize + проверяется → второй args block'ируется → команда block с reason про второй args. Все reading commands проверяются единообразно (не только `cat`). **File-watcher для script execution — session-scoped, NOT turn-scoped:** gate tracks `edited_files_this_session` в `~/.claude/runtime/edited-files-.json` (session-wide persistence). Если в session был Edit/Write на `*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` файл, последующие `node X`, `npx vitest run X`, `python X` блокируются если target path в session-list. Reset происходит **только** на: @@ -760,12 +770,14 @@ Protected paths должны быть live **до** того как соотве | Этап | Protected-up-to | Защиты live | |---|---|---| | 1 (pure decision module) | settings.json только | Gate в shadow-mode (logs, не блокирует) | -| 1.4 (path normalization) | + `~/.claude/runtime/*` + `tools/enforce-*.mjs` | Hard-deny на gate state-файлы | -| 2 (удаление 5 хуков + vocab) | + `docs/registry/nodes.yaml` + `tools/registry-load.mjs` + `tools/enforce-hook-helpers.mjs` + `tools/subagent-prompt-prefix.mjs` + `package.json` / `composer.json` + `gate-config.json` explicit | Полный §3.1 list active **до** включения router-gate (subagent-prompt-prefix должен быть в protected ДО первой записи new-schema inheritance файла) | +| 1.4 (path normalization) | + `~/.claude/runtime/*` (включая `gate-config.json` — early protection, closes audit DOS D-1) + `tools/enforce-*.mjs` | Hard-deny на gate state-файлы; gate-config.json защищён ДО Этапа 2 чтобы tiny-budget patch attack невозможна | +| 2 (удаление 5 хуков + vocab) | + `docs/registry/nodes.yaml` + `tools/registry-load.mjs` + `tools/enforce-hook-helpers.mjs` + `tools/subagent-prompt-prefix.mjs` + `package.json` / `composer.json` | Полный §3.1 list active **до** включения router-gate (subagent-prompt-prefix должен быть в protected ДО первой записи new-schema inheritance файла) | | 2.1 (subagent inheritance) | (без новых) | §3.2.0 smoke-tests run by user; если pass — env-based реализация активируется | -| 3 (settings.json reg) | (без новых) | Router-gate switch from shadow to enforce-mode | +| 2.2 (subagent constraints + block-file) | (без новых) | §3.4 constraints + side-channel block-file активны | +| **2.3 (branch-switch AskUser-gate)** | (без новых) | **MUST complete BEFORE Этап 3** (closes audit CRITICAL-10): `enforce-branch-switch.mjs` rewrite under §4.5 git-pattern + askuser-decisions consume — иначе во время Этапа 3 enforce-mode OLD hook ещё принимает `BRANCH-SWITCH-CONFIRMED` controller-writable marker → S8 closure НЕ активна → dangerous git ops unprotected. Альтернатива: Этап 3 settings.json регистрация включает обновлённый branch-switch hook одной транзакцией | +| 3 (settings.json reg) | (без новых) | Router-gate switch from shadow to enforce-mode. **Pre-condition:** Этап 2.3 deployed (S8 closure live). Если 2.3 не готов → НЕ переходить в enforce-mode | -**Принцип:** каждый Этап **сначала** расширяет protected list, **потом** активирует защиту что на него ссылается. Между Этапами — gap-окно когда controller теоретически может Edit'нуть будущий protected file. Mitigation: Этапы 1.4 → 2 → 2.1 → 3 идут одной сессией (несколько часов), не дни — gap-окно short. +**Принцип:** каждый Этап **сначала** расширяет protected list, **потом** активирует защиту что на него ссылается. Между Этапами — gap-окно когда controller теоретически может Edit'нуть будущий protected file. Mitigation: Этапы 1.4 → 2 → 2.1 → 2.2 → 2.3 → 3 идут одной сессией (несколько часов), не дни — gap-окно short. ### Риски миграции @@ -782,6 +794,18 @@ Protected paths должны быть live **до** того как соотве - **Logging granularity** — все decision events в одном файле или раздельно (router-gate / askuser-decisions / gate-errors)? Сейчас разделено на 3 файла. - **Bash whitelist completeness** — список в §5.1 покрывает основные read-only operations, но возможно есть пропуски (например `tree`, `du`, `find` с right args). Расширяется итеративно из реальной работы. +### Audit findings deferred to writing-plans (5 SHOULD fixes) + +29.05.2026 adversarial audit ([docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 5 findings которые требуют **design decisions per fix** и решаются в writing-plans фазе (не trivial inline patch): + +1. **SHOULD-FIX-1 — vitest globalSetup cross-session blind spot** (audit CRITICAL-2): Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state. Open question для plan: scan recursion limit (1-level transitive imports follow vs full transitive). +2. **SHOULD-FIX-2 — `git format-patch -o` handling verification** (audit CRITICAL-7): row label «git log / git show / git diff / git blame / git format-patch» с exclusion `-o ` может покрывать обе команды (row-wide application) или только первую (per-command implementation). Verify parser implementation в Этапе 1.1 (Bash content parser); если per-command — добавить explicit `git format-patch` row с `-o` exclusion И блокировкой write в protected paths. +3. **SHOULD-FIX-3 — `approved_action_pattern` mandatory file-path binding** (audit PARTIAL-1): §4.5 answer parser сейчас разрешает class-level unlock (`approved_tool: "Edit"` без path). Plan: каждое `approved_specific_tool` interpretation обязано извлечь `target_path` из user choice и сохранить как `approved_action_pattern`. Gate при mutating tool сравнивает `tool_input.file_path` против pattern (exact или prefix match). Open question: glob-style patterns (`docs/**`) разрешены или только exact? +4. **SHOULD-FIX-4 — Chain reset фразы только в organic root prompt** (audit PARTIAL-2): §3 chain-state TTL detection. AskUser chosen_label содержащий `новая задача` сейчас тригернёт reset. Plan: интегрировать с CRITICAL-1 fix (transcript metadata `user_message_type`) — chain reset check только на `user_message_type: "prompt"`. Open question: что делать если controller пишет AskUserQuestion option с явным `"начать новую задачу"` — block через §4.7 question quality detector или allow с warning? +5. **SHOULD-FIX-5 — chain-state malformed → fail-CLOSE** (audit PARTIAL-7): §3.3 failure mode table сейчас даёт «chain-state malformed → reset+warn» (fail-OPEN). Symmetric fix с router-state semantics. Open question: какой recovery path если chain-state легитимно corrupted (например crash mid-write) — manual reset через recovery §6 уровень 3 или auto-reset с явной user confirmation через AskUser? + +Audit также identified 9 DOS findings + 15 INFO/acknowledged residuals — детали в audit report. Plan ОБЯЗАН адресовать минимум 5 DOS findings: D-2 (subagent success-marker file), D-3 (lefthook exit code parsing), D-7 (subagent gate reader-writer lock), D-8 (gate-config defaults fallback), D-9 (enforce-prompt-injection empty handling). + ### Acceptable residuals (acknowledged design choices, не security holes) - **Max 2 AskUserQuestion per turn limit** (§4.5): chain'ы с many mutating tools (>2 шагов с Edit/Write/Bash per ход) НЕ помещаются в один turn — заказчик вынужден re-prompt'ить или делить задачу. Acceptable UX-tradeoff — это design choice §4.5 (anti-fatigue), не дыра. Multi-step chains реально работают через переspawning context per next prompt. Документировать в README/onboarding.