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
|
|
@ -270,6 +270,35 @@ function readLastEntries(stateFile, n) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a window covering the last n TOOL-CALL entries plus any marker entries
|
||||
* interleaved within that span.
|
||||
*
|
||||
* v7.8.3 #10 fix: the primary trifecta detector previously used
|
||||
* readLastEntries(stateFile, WINDOW_SIZE), which counts raw lines with no type
|
||||
* filter. Marker entries (warning/volume_warning/escalation_warning/...)
|
||||
* appended to the same file diluted the 20-line window and scrolled real
|
||||
* trifecta legs out — a false negative. This helper counts actual tool-call
|
||||
* entries (no `type` field) and returns the slice from the n-th-last tool call
|
||||
* onward, keeping interleaved markers so dedup checks (hasRecentWarning) still
|
||||
* see them.
|
||||
* @param {string} stateFile
|
||||
* @param {number} n - number of tool-call entries the window must cover
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function readToolCallWindow(stateFile, n) {
|
||||
const all = readLastEntries(stateFile, 10_000);
|
||||
let toolCount = 0;
|
||||
let start = 0;
|
||||
for (let i = all.length - 1; i >= 0; i--) {
|
||||
if (!all[i].type) {
|
||||
toolCount++;
|
||||
if (toolCount === n) { start = i; break; }
|
||||
}
|
||||
}
|
||||
return all.slice(start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up state files older than CLEANUP_MAX_AGE_MS.
|
||||
* Only called on first invocation per session (when state file doesn't exist yet).
|
||||
|
|
@ -860,7 +889,9 @@ const messages = [];
|
|||
|
||||
// --- Trifecta detection (skip for neutral-only and delegation-only calls) ---
|
||||
if (!(classes.length === 1 && (classes[0] === 'neutral' || classes[0] === 'delegation'))) {
|
||||
const window = readLastEntries(stateFile, WINDOW_SIZE);
|
||||
// v7.8.3 #10: count tool-call entries, not raw lines — marker entries must
|
||||
// not dilute the trifecta window (see readToolCallWindow).
|
||||
const window = readToolCallWindow(stateFile, WINDOW_SIZE);
|
||||
const { detected, evidence } = checkTrifecta(window);
|
||||
|
||||
if (detected && !hasRecentWarning(window)) {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,13 @@ const BLOCK_RULES = [
|
|||
{
|
||||
name: 'Pipe-to-shell (curl|sh, wget|sh, curl|bash)',
|
||||
// Matches: curl ... | sh, curl ... | bash, wget ... | sh, etc.
|
||||
// Also catches variations with xargs sh, xargs bash
|
||||
pattern: /(?:curl|wget)\b[^|]*\|\s*(?:bash|sh|zsh|ksh|dash)\b/,
|
||||
// v7.8.3 #12: also catches a shell reached through interposition —
|
||||
// intermediate pipe stages (curl x | tee y | sh) and wrapper commands
|
||||
// with optional flags/assignments (xargs sh, sudo -E bash, env FOO=1 sh,
|
||||
// nohup bash, command sh — chains like `xargs sudo bash` included).
|
||||
// The intermediate-segment group requires a non-empty segment so `||`
|
||||
// (shell OR, e.g. `curl x || sh fallback.sh`) does not match.
|
||||
pattern: /(?:curl|wget)\b[^|]*\|(?:[^|]+\|)*\s*(?:(?:sudo|xargs|env|nohup|command)(?:\s+(?:-{1,2}[\w=/.-]+|\w+=\S*))*\s+)*(?:bash|sh|zsh|ksh|dash)\b/,
|
||||
description:
|
||||
'Piping remote content directly into a shell interpreter allows ' +
|
||||
'arbitrary remote code execution without inspection. Download the script first, ' +
|
||||
|
|
|
|||
|
|
@ -27,12 +27,24 @@ const SECRET_PATTERNS = [
|
|||
{ name: 'Azure AI Services Key', pattern: /Ocp-Apim-Subscription-Key\s*[=:]\s*['"]?[0-9a-f]{32}['"]?/i },
|
||||
{ name: 'GitHub Token', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/ },
|
||||
{ name: 'npm Token', pattern: /npm_[A-Za-z0-9]{36}/ },
|
||||
// v7.8.3 #13 — bare provider keys (see knowledge/secrets-patterns.md).
|
||||
// Previously these were caught only when wrapped in a quoted label
|
||||
// assignment (password|secret|token|api_key = "..."); the bare key forms
|
||||
// slipped through.
|
||||
{ name: 'Anthropic API Key', pattern: /\bsk-ant-api03-[A-Za-z0-9_-]{93}\b/ },
|
||||
{ name: 'OpenAI Project Key', pattern: /\bsk-proj-[A-Za-z0-9_-]{40,}\b/ },
|
||||
{ name: 'GitHub Fine-Grained PAT', pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
|
||||
{ name: 'Google API Key', pattern: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
||||
{ name: 'Private Key PEM Block', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ },
|
||||
{ name: 'JWT Secret', pattern: /JWT[_-]?SECRET\s*[=:]\s*['"][^'"]{8,}['"]/i },
|
||||
{ name: 'Slack/Discord Webhook URL', pattern: /https:\/\/(?:hooks\.slack\.com\/services|discord(?:app)?\.com\/api\/webhooks)\// },
|
||||
{ name: 'Generic credential assignment', pattern: /(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*['"][^'"]{8,}['"]/i },
|
||||
{ name: 'Authorization header with token', pattern: /[Bb]earer [A-Za-z0-9\-._~+/]{20,}/ },
|
||||
{ name: 'Database connection string', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^\s]+@[^\s]+/i },
|
||||
// v7.8.3 #13 — three-part JWT (header.payload.signature, base64url). The
|
||||
// 10-char part minimum keeps prose fragments (eyJabc.def.ghi) from tripping.
|
||||
// Kept last so a Bearer-header context reports as 'Authorization header'.
|
||||
{ name: 'JWT (three-part token)', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
||||
// Policy-defined additional patterns
|
||||
...getPolicyValue('secrets', 'additional_patterns', []).map((p, i) => ({
|
||||
name: `Custom pattern ${i + 1}`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue