fix(llm-security): compileRules coerces a non-string pattern instead of dropping the rule
new RegExp(pattern, 'i') never throws when pattern is a truthy non-string
(e.g. an object) — it ToString-coerces it first. The truthy-only guard
(`!rule.pattern`) let such a rule through as a real, compiled RegExp,
bypassing the try/catch meant to drop malformed rules. Worse than a silent
drop: `new RegExp("[object Object]", "i")` is parsed as a character class
over o/b/j/e/c/t/space, so the "dropped" rule instead becomes a
near-universal false-positive matcher. Same path for the built-in
commons-backed ruleset and the operator's sig.custom_rules_path (both
route through compileRules).
Fix: require typeof rule.pattern === 'string' before compiling. Verified
the golden dump pins no rule that exists only because of this coercion —
it regenerates byte-identically after the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iWrdLSVgRhgPzTB29rQGD
This commit is contained in:
parent
eceb71bbb3
commit
f926071348
3 changed files with 63 additions and 5 deletions
|
|
@ -51,10 +51,15 @@ import { loadArtifact } from './commons-loader.mjs';
|
|||
* this defaulting logic would drift, and the custom path is the one an
|
||||
* operator can get wrong.
|
||||
*
|
||||
* Each rule's `pattern` is compiled case-insensitively; rules lacking `id` or
|
||||
* `pattern`, and rules whose pattern fails to compile, are dropped. The
|
||||
* defaults below are loader tolerance, recorded in the artifact under
|
||||
* `missing-field-defaults` — not an optional-field contract.
|
||||
* Each rule's `pattern` is compiled case-insensitively; rules lacking `id`,
|
||||
* lacking a non-empty string `pattern`, or whose pattern fails to compile, are
|
||||
* dropped. The string check matters on its own: `new RegExp` does not throw on
|
||||
* a non-string `pattern` (e.g. `{source: 'x'}`) — it coerces the value via
|
||||
* ToString first, so a vendored or operator-supplied rule with a malformed
|
||||
* (non-string) pattern would otherwise compile into a real, unintended regex
|
||||
* instead of being caught by the try/catch below. The defaults below are
|
||||
* loader tolerance, recorded in the artifact under `missing-field-defaults` —
|
||||
* not an optional-field contract.
|
||||
*
|
||||
* @param {object} parsed
|
||||
* @returns {Array<{id: string, family: string, severity: string, re: RegExp,
|
||||
|
|
@ -63,7 +68,7 @@ import { loadArtifact } from './commons-loader.mjs';
|
|||
export function compileRules(parsed) {
|
||||
const compiled = [];
|
||||
for (const rule of parsed?.rules || []) {
|
||||
if (!rule || !rule.id || !rule.pattern) continue;
|
||||
if (!rule || !rule.id || typeof rule.pattern !== 'string' || !rule.pattern) continue;
|
||||
let re;
|
||||
try {
|
||||
re = new RegExp(rule.pattern, 'i');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue