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 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-31 13:34:42 +03:00
parent 63100decce
commit 30b79c7228
2 changed files with 51 additions and 0 deletions
+8
View File
@@ -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) {
+43
View File
@@ -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');
});
});