fix(llm-security): hook coverage — pathguard on Edit, trifecta window, pipe-to-shell, provider keys (#9,#10,#12,#13,#11-doc)
#9 pathguard registered matcher Write only, so an Edit/MultiEdit to an existing protected file (settings.json, .env, .ssh, the hooks themselves) bypassed it entirely; matcher now Edit|Write (the script already reads only tool_input.file_path). #10 the primary trifecta detector's 20-entry window counted marker lines, so accumulated markers scrolled a real leg out (false negative); the window now counts tool-call entries. #13 pre-edit-secrets caught bare provider keys only inside a quoted label assignment; added anchored patterns for Anthropic sk-ant, OpenAI sk-proj, fine-grained github_pat_, Google AIza, and JWT eyJ (minimum-length guarded against prose false positives). #12 the remote-pipe-to-shell block required a shell immediately after the first pipe, so xargs/sudo/tee/env interposition evaded it and the comment falsely claimed xargs was caught; broadened to reach a shell through intermediate segments while leaving shell-OR fallbacks unblocked. #11 (doc only): knowledge/owasp-skills-top10.md claimed pre-bash-destructive blocks persistence commands — it does not; corrected to mark persistence detection as unimplemented/future (the detector itself is deferred to v8). Suite 2004/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
parent
21c6c2b534
commit
8a59d616fb
9 changed files with 337 additions and 6 deletions
|
|
@ -4,14 +4,20 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { resolve } from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { runHook } from './hook-helper.mjs';
|
||||
|
||||
const SCRIPT = resolve(import.meta.dirname, '../../hooks/scripts/pre-write-pathguard.mjs');
|
||||
const HOOKS_JSON = resolve(import.meta.dirname, '../../hooks/hooks.json');
|
||||
|
||||
function writePayload(filePath) {
|
||||
return { tool_name: 'Write', tool_input: { file_path: filePath, content: 'data' } };
|
||||
}
|
||||
|
||||
function editPayload(filePath) {
|
||||
return { tool_name: 'Edit', tool_input: { file_path: filePath, old_string: 'a', new_string: 'b' } };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BLOCK cases — exit code 2
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -170,3 +176,51 @@ describe('pre-write-pathguard — ALLOW cases', () => {
|
|||
assert.equal(result.code, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edit tool coverage (v7.8.3 #9) — the pathguard was registered with matcher
|
||||
// `Write` only, so modifying an EXISTING protected file via Edit bypassed all
|
||||
// pathguard protections (hook tampering, settings.json, .env/.ssh writes).
|
||||
// The script itself is tool-agnostic (it only reads tool_input.file_path);
|
||||
// the registration in hooks.json must cover Edit too.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('pre-write-pathguard — Edit tool coverage (#9)', () => {
|
||||
it('hooks.json registers the pathguard with matcher Edit|Write', () => {
|
||||
const parsed = JSON.parse(readFileSync(HOOKS_JSON, 'utf-8'));
|
||||
const entry = (parsed.hooks?.PreToolUse ?? []).find(e =>
|
||||
(e.hooks ?? []).some(h => (h.command ?? '').includes('pre-write-pathguard.mjs'))
|
||||
);
|
||||
assert.ok(entry, 'pathguard must be registered under PreToolUse');
|
||||
assert.equal(
|
||||
entry.matcher,
|
||||
'Edit|Write',
|
||||
'pathguard matcher must cover Edit as well as Write — Edit to an existing protected file bypasses a Write-only matcher'
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks an Edit payload targeting .env (environment file)', async () => {
|
||||
const result = await runHook(SCRIPT, editPayload('/project/.env'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
});
|
||||
|
||||
it('blocks an Edit payload targeting a hooks/scripts/*.mjs path (hook tampering)', async () => {
|
||||
const result = await runHook(SCRIPT, editPayload('/project/hooks/scripts/my-hook.mjs'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
assert.match(result.stderr, /hooks/i);
|
||||
});
|
||||
|
||||
it('blocks an Edit payload targeting .claude/settings.json (settings file)', async () => {
|
||||
const result = await runHook(SCRIPT, editPayload('/home/user/.claude/settings.json'));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /PATH GUARD/);
|
||||
assert.match(result.stderr, /settings/i);
|
||||
});
|
||||
|
||||
it('allows an Edit payload targeting a normal source file', async () => {
|
||||
const result = await runHook(SCRIPT, editPayload('src/app.js'));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue