fix(llm-security): scanner false-positives/negatives — trigger, toxic-flow, policy-loader (#38-#41,#57,#26)
#38 toxic-flow matched trifecta-leg keywords with bare includes(), so substrings ('url' in 'curl', 'key' in 'monkey', 'auth' in 'author') fabricated CRITICAL trifectas on benign components; now word-boundary matched. #40 TRG-broad fired HIGH on a bare any/all/every anywhere ('fix any lint errors'); the universal-claim regex now requires genuine universal phrasing. #41 TRG-baiting substring-matched ('any file' in 'many files'); now boundary-anchored. #39 the broad-name list missed multi-char generic names (helper/assistant/auto/general/agent/tool); widened coherently so it does not reintroduce #40. #57 the '(recovered from obfuscation)' label compared raw against a lowercased normal form, firing on any uppercase char; now gated on an explicit decode-changed flag. #26 (same file) getPolicyValue used 'key in sectionObj' with no type guard, so a scalar section override in policy.json (e.g. {"injection":"block"}) threw an uncaught TypeError; now guarded to fall back to the default. Suite 1931/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
196517f38a
commit
f3aaf5479f
6 changed files with 339 additions and 13 deletions
|
|
@ -64,6 +64,26 @@ const EXFIL_KEYWORDS = [
|
|||
'network', 'api', 'endpoint', 'transfer', 'exfil',
|
||||
];
|
||||
|
||||
/**
|
||||
* Compile a keyword into a word-boundary-anchored regex (#38), so 'url' no
|
||||
* longer matches inside 'curl', 'key' inside 'monkey', 'auth' inside
|
||||
* 'author', or 'api' inside 'rapidly'. Boundary guards are only applied
|
||||
* where the keyword edge is a word character (so '.env' still matches
|
||||
* 'config.env'); a trailing plural 's' is allowed ('credentials', 'tokens').
|
||||
*/
|
||||
function keywordToRegex(kw) {
|
||||
const escaped = String(kw).split(/\s+/)
|
||||
.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('\\s+');
|
||||
const lead = /^[a-z0-9_$]/i.test(kw) ? '(?<![a-z0-9_$])' : '';
|
||||
const trail = /[a-z0-9_]$/i.test(kw) ? 's?(?![a-z0-9_])' : '';
|
||||
return new RegExp(lead + escaped + trail, 'i');
|
||||
}
|
||||
|
||||
const INPUT_KEYWORD_MATCHERS = INPUT_KEYWORDS.map(kw => ({ kw, re: keywordToRegex(kw) }));
|
||||
const SENSITIVE_KEYWORD_MATCHERS = SENSITIVE_KEYWORDS.map(kw => ({ kw, re: keywordToRegex(kw) }));
|
||||
const EXFIL_KEYWORD_MATCHERS = EXFIL_KEYWORDS.map(kw => ({ kw, re: keywordToRegex(kw) }));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook guard patterns — known hooks that mitigate exfil paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -234,9 +254,9 @@ function classifyTrifectaLegs(components, priorResults, mcpPresent) {
|
|||
comp.inputEvidence.push('$ARGUMENTS in command body');
|
||||
}
|
||||
|
||||
// Keyword-based
|
||||
for (const kw of INPUT_KEYWORDS) {
|
||||
if (comp.description.includes(kw) || comp.body.includes(kw)) {
|
||||
// Keyword-based (word-boundary matched, #38)
|
||||
for (const { kw, re } of INPUT_KEYWORD_MATCHERS) {
|
||||
if (re.test(comp.description) || re.test(comp.body)) {
|
||||
comp.hasInputSurface = true;
|
||||
comp.inputEvidence.push(`keyword "${kw}"`);
|
||||
break;
|
||||
|
|
@ -267,8 +287,8 @@ function classifyTrifectaLegs(components, priorResults, mcpPresent) {
|
|||
}
|
||||
}
|
||||
|
||||
for (const kw of SENSITIVE_KEYWORDS) {
|
||||
if (comp.description.includes(kw) || comp.body.includes(kw)) {
|
||||
for (const { kw, re } of SENSITIVE_KEYWORD_MATCHERS) {
|
||||
if (re.test(comp.description) || re.test(comp.body)) {
|
||||
comp.hasDataAccess = true;
|
||||
comp.accessEvidence.push(`keyword "${kw}"`);
|
||||
break;
|
||||
|
|
@ -290,8 +310,8 @@ function classifyTrifectaLegs(components, priorResults, mcpPresent) {
|
|||
comp.exfilEvidence.push(`delegation: ${matched.join(', ')} (can spawn capable sub-agents)`);
|
||||
}
|
||||
|
||||
for (const kw of EXFIL_KEYWORDS) {
|
||||
if (comp.description.includes(kw) || comp.body.includes(kw)) {
|
||||
for (const { kw, re } of EXFIL_KEYWORD_MATCHERS) {
|
||||
if (re.test(comp.description) || re.test(comp.body)) {
|
||||
comp.hasExfilSink = true;
|
||||
comp.exfilEvidence.push(`keyword "${kw}"`);
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue