From 30b79c7228ebbb9c0ec2f8fcef31e65a33f0d953 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: Sun, 31 May 2026 13:34:42 +0300 Subject: [PATCH] fix(router-gate): narrow cd app whitelist (TDD, tools 1978 GREEN) Add /^cd\s+app$/ to SAFE_EXACT so already-whitelisted commands (pest, php artisan test) run from app/. Scope limited to the literal `app` dir: cd into any other path (incl. protected .claude/runtime, memory/, transcripts) stays default-deny, so the cwd-shift read-bypass is contained. Mutations remain caught at the hard-blacklist + chain-mutating rule, and each chain segment after `cd app &&` must still be independently whitelisted. Owner-authorized, narrow scope = literal `app` only. Co-Authored-By: Claude Opus 4.8 --- tools/enforce-router-gate.mjs | 8 ++++++ tools/enforce-router-gate.test.mjs | 43 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/tools/enforce-router-gate.mjs b/tools/enforce-router-gate.mjs index 399a58bd..4fcfdb45 100644 --- a/tools/enforce-router-gate.mjs +++ b/tools/enforce-router-gate.mjs @@ -95,6 +95,14 @@ const SAFE_EXACT = [ /^composer\s+(?:test|pint|stan|insights|rector)\b/, /^(?:\.\/)?vendor\/bin\/pest\b/, /^pest\b/, + // Narrow `cd app` (2026-05-31, owner-authorized) — enter the Laravel project dir + // so already-whitelisted commands (pest, php artisan test) run from app/. + // Scope deliberately limited to the literal `app` dir: `cd` into any other path + // (incl. protected .claude/runtime, memory/, transcripts) stays default-deny, so + // the cwd-shift read-bypass is contained. Mutations remain caught at the + // hard-blacklist + chain-mutating rule (both run before the whitelist), and each + // chain segment after `cd app &&` must still be independently whitelisted. + /^cd\s+app$/, ]; export function classifyWhitelist(segments) { diff --git a/tools/enforce-router-gate.test.mjs b/tools/enforce-router-gate.test.mjs index 0c9984ed..0207a70d 100644 --- a/tools/enforce-router-gate.test.mjs +++ b/tools/enforce-router-gate.test.mjs @@ -227,3 +227,46 @@ describe('SAFE_EXACT — Laravel dev workflow (whitelist expansion 2026-05-30)', expect(classifyBashCommand('composer outdated', {}).result).toBe('allow'); }); }); + +describe('SAFE_EXACT — narrow `cd app` whitelist (2026-05-31, owner-authorized)', () => { + // Allowed: enter the Laravel project dir, alone or chained with whitelisted cmds + it.each([ + 'cd app', + 'cd app && pest', + 'cd app && php artisan test', + 'cd app && composer test', + ])('allows %s', (cmd) => { + expect(classifyBashCommand(cmd, {}).result).toBe('allow'); + }); + + // Scope: cd into any other dir stays default-deny (cwd-shift read-bypass contained) + it.each([ + 'cd ~/.claude/runtime', + 'cd ../memory', + 'cd app/storage', + 'cd /tmp', + 'cd ..', + ])('still blocks cd into non-app dir: %s', (cmd) => { + expect(classifyBashCommand(cmd, {}).result).toBe('block'); + }); + + // cwd-shift read-exfil attempt via narrow cd app stays blocked (protected path by name) + it('still blocks reading a protected file from app/ via literal path', () => { + expect(classifyBashCommand('cd app && cat ../.env', {}).result).toBe('block'); + expect(classifyBashCommand('cd app && cat ~/.claude/runtime/state.json', {}).result).toBe('block'); + }); + + // Mutations after cd app remain caught (hard-blacklist + chain-mutating rule) + it.each([ + 'cd app && rm foo', + 'cd app && mkdir x', + 'cd app && git commit -m x', + ])('still blocks mutating chain: %s', (cmd) => { + expect(classifyBashCommand(cmd, {}).result).toBe('block'); + }); + + // Second segment must still be independently whitelisted + it('still blocks cd app chained with a non-whitelisted command', () => { + expect(classifyBashCommand('cd app && frobnicate', {}).result).toBe('block'); + }); +});