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
|
|
@ -37,6 +37,28 @@ const bearerLine = [
|
|||
'eyJhbGciOiJSUzI1NiJ9.payload.sig12345678',
|
||||
].join('');
|
||||
|
||||
// v7.8.3 #13 — bare provider keys (documented in knowledge/secrets-patterns.md)
|
||||
// that previously slipped through unless wrapped in a quoted label assignment.
|
||||
|
||||
// Anthropic API key: sk-ant-api03- + 93 chars of [A-Za-z0-9_-]
|
||||
const anthropicKey = ['sk-ant-', 'api03-', 'x'.repeat(92) + 'Q'].join('');
|
||||
|
||||
// OpenAI project key: sk-proj- + >= 40 chars of [A-Za-z0-9_-]
|
||||
const openaiProjKey = ['sk-', 'proj-', 'Ab1'.repeat(14)].join('');
|
||||
|
||||
// GitHub fine-grained PAT: github_pat_ + 82 chars of [A-Za-z0-9_]
|
||||
const githubFinePat = ['github_', 'pat_', 'A1'.repeat(41)].join('');
|
||||
|
||||
// Google API key: AIza + exactly 35 chars of [0-9A-Za-z_-]
|
||||
const googleApiKey = ['AIza', 'Sy' + 'D'.repeat(33)].join('');
|
||||
|
||||
// JWT: three base64url parts separated by dots, header starts with eyJ
|
||||
const jwtToken = [
|
||||
'eyJ', 'hbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
||||
'.', 'eyJzdWIiOiIxMjM0NTY3ODkwIn0',
|
||||
'.', 'TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
|
||||
].join('');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -105,6 +127,95 @@ describe('pre-edit-secrets — BLOCK cases', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// v7.8.3 #13 — bare provider keys must block WITHOUT a label assignment
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('pre-edit-secrets — bare provider keys (#13)', () => {
|
||||
it('blocks a bare Anthropic API key (sk-ant-api03-...)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'src/config.js',
|
||||
`const k = "${anthropicKey}";`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /Anthropic/);
|
||||
});
|
||||
|
||||
it('blocks a bare OpenAI project key (sk-proj-...)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'src/config.js',
|
||||
`const k = "${openaiProjKey}";`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /OpenAI/);
|
||||
});
|
||||
|
||||
it('blocks a bare fine-grained GitHub PAT (github_pat_...)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'src/config.js',
|
||||
`const k = "${githubFinePat}";`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /GitHub/);
|
||||
});
|
||||
|
||||
it('blocks a bare Google API key (AIza...)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'src/config.js',
|
||||
`const k = "${googleApiKey}";`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /Google/);
|
||||
});
|
||||
|
||||
it('blocks a bare JWT (eyJ...header.payload.signature)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'src/config.js',
|
||||
`const t = "${jwtToken}";`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /JWT/);
|
||||
});
|
||||
|
||||
// FP probes — prose mentioning the prefixes must not trip the patterns.
|
||||
it('allows prose mentioning the sk-ant- prefix without a key body', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'docs/notes.md',
|
||||
'Anthropic keys start with sk-ant- and must never be committed.'
|
||||
));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
it('allows prose mentioning the AIza prefix without a key body', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'docs/notes.md',
|
||||
'Google API keys have the AIza prefix (39 chars total).'
|
||||
));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
it('allows prose mentioning the github_pat_ prefix without a key body', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'docs/notes.md',
|
||||
'Fine-grained PATs use the github_pat_ prefix.'
|
||||
));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
|
||||
it('allows prose mentioning eyJ with short/dotted fragments (not a real JWT)', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'docs/notes.md',
|
||||
'JWTs start with eyJ... e.g. eyJabc.def.ghi is too short to be real.'
|
||||
));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ALLOW cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue