fix(hooks): close F-5 path-traversal hardening in lib.mjs, split by field
session_id becomes a raw filename segment in sessionStateFile(), so an unvalidated value could escape STATE_DIR via path traversal (verified with a failing test before the fix). Now allowlisted to ^[A-Za-z0-9_-]+$, with invalid values degrading to a fixed sentinel filename rather than blocking the hook. cwd is a base directory, not a segment, and every real value contains "/" — applying the same allowlist as the review's literal suggestion would reject all legitimate absolute paths and silently disable the project-level config override. initConfig() instead guards with isAbsolute(cwd) && no NUL byte. Both harness-supplied, not user-controlled: defense-in-depth, not a fix for an observed exploit. Tests added for the escape (red before fix, green after) and for the cwd regression (a normal absolute cwd still loads project config). Full resolution notes in docs/review-2026-06-20.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSATejUPjGaxGnj9jkFQTo
This commit is contained in:
parent
b4898746c1
commit
736a1c0deb
4 changed files with 93 additions and 7 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { describe, it, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { writeFileSync, mkdtempSync, mkdirSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
|
||||
|
||||
let dir;
|
||||
|
|
@ -70,6 +71,28 @@ describe('session-start', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('initConfig — cwd path handling (F-5 regression guard)', () => {
|
||||
let projectDir;
|
||||
|
||||
afterEach(() => { if (projectDir) rmSync(projectDir, { recursive: true, force: true }); });
|
||||
|
||||
it('still loads project-level config for a normal absolute cwd', () => {
|
||||
dir = setupTestDir();
|
||||
projectDir = mkdtempSync(join(tmpdir(), 'ia-project-'));
|
||||
mkdirSync(join(projectDir, '.claude'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectDir, '.claude', 'ai-psychosis.local.md'),
|
||||
'---\nlayer2: false\n---\n'
|
||||
);
|
||||
|
||||
const out = runHook('session-start.mjs', { session_id: 's-cfg', cwd: projectDir }, dir);
|
||||
// layer2 disabled by the project config -> requireLayer(2) short-circuits
|
||||
// before any hookSpecificOutput is emitted.
|
||||
assert.equal(out.continue, true);
|
||||
assert.ok(!out.hookSpecificOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tier-2 cross-session alert ---
|
||||
//
|
||||
// Fires at SessionStart when last 3 end records all have user_info_class='no'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue