You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[CLAUDE ROUTINE]: Security enhancement — bound the quantifiers in LOCAL_IMPORT_RE / LOCAL_REQUIRE_RE so a hand-crafted policy file can't ReDoS the custom-hook loader #249
The custom-hook loader scans every reachable policy file with two regexes that contain unbounded [\s\S]*? segments. They work great on real-world import / require lines, but on pathological input (a single 100 KB line with many quote characters) the engine can backtrack catastrophically. This is the classic ReDoS shape, and since we run this regex over user-authored .js files (which we already invite users to write), bounding the quantifier removes a sharp edge before it cuts anyone.
Note: this is distinct from #137, which targets user-supplied patterns inside sanitize-api-keys. The issue here is in our own code that parses user policy files.
Where
src/hooks/loader-utils.ts:21,24
/** Regex to find local relative import specifiers (ESM). */constLOCAL_IMPORT_RE=/(?:import\s+(?:[\s\S]*?\s+from\s+)?|export\s+(?:[\s\S]*?\s+from\s+))(['"])(\.\.?\/[^'"]+)\1/g;/** Regex to find local relative require specifiers (CJS). */constLOCAL_REQUIRE_RE=/require\s*\(\s*(['"])(\.\.?\/[^'"]+)\1\s*\)/g;
LOCAL_IMPORT_RE is the riskier one — [\s\S]*?\s+from\s+ is lazy but the engine still has to try every backtrack point if from never appears on a long line. LOCAL_REQUIRE_RE is shape-bounded but worth tightening for symmetry.
Used by rewriteFileTree() (src/hooks/custom-hooks-loader.ts) — invoked on every hook event when custom policies are configured.
Why this helps users
sequenceDiagram
participant CC as Claude Code
participant H as Hook handler
participant L as Custom hook loader
participant FS as Disk
CC->>H: PreToolUse event
H->>L: rewriteFileTree(entry)
L->>FS: read user policy.js
FS-->>L: 80 KB single-line file
L->>L: LOCAL_IMPORT_RE.exec(code)
Note over L: Backtracking explosion\nhandler exceeds 10s timeout\n→ falls back to "allow"
L-->>H: timeout
H-->>CC: allow (silently degraded)
Loading
Predictable hook latency — [\s\S]{0,N}? keeps worst-case parse linear in line length.
Loader stays correct on minified files — ironically, a minified bundled policy can already trip this if a user copy-pastes one.
No regression — import ... from "..." statements are short by design; a small ceiling (e.g. 2 KB) is well above realistic specifiers.
Proposed enhancement
Replace the inner unbounded quantifier with a bounded one that comfortably covers real imports:
Add a focused unit test under __tests__/hooks/loader-utils.test.ts:
Normal ESM/CJS imports still match (regression).
64 KB single line of ' and " characters returns within 50 ms.
Acceptance criteria
Bounded quantifier in both LOCAL_IMPORT_RE and LOCAL_REQUIRE_RE.
Optional length guard with hookLogWarn so power users see why their giant generated file was skipped.
Unit test with timing assertion (expect(duration).toBeLessThan(50)).
CHANGELOG.md entry under Unreleased → Fixes.
Severity
Low — exploitable only by self-authored policy files (which already run arbitrary code), but cheap to fix and removes a foot-gun on copy-pasted/minified files.
Summary
The custom-hook loader scans every reachable policy file with two regexes that contain unbounded
[\s\S]*?segments. They work great on real-worldimport/requirelines, but on pathological input (a single 100 KB line with many quote characters) the engine can backtrack catastrophically. This is the classic ReDoS shape, and since we run this regex over user-authored.jsfiles (which we already invite users to write), bounding the quantifier removes a sharp edge before it cuts anyone.Note: this is distinct from #137, which targets user-supplied patterns inside
sanitize-api-keys. The issue here is in our own code that parses user policy files.Where
src/hooks/loader-utils.ts:21,24LOCAL_IMPORT_REis the riskier one —[\s\S]*?\s+from\s+is lazy but the engine still has to try every backtrack point iffromnever appears on a long line.LOCAL_REQUIRE_REis shape-bounded but worth tightening for symmetry.Used by
rewriteFileTree()(src/hooks/custom-hooks-loader.ts) — invoked on every hook event when custom policies are configured.Why this helps users
sequenceDiagram participant CC as Claude Code participant H as Hook handler participant L as Custom hook loader participant FS as Disk CC->>H: PreToolUse event H->>L: rewriteFileTree(entry) L->>FS: read user policy.js FS-->>L: 80 KB single-line file L->>L: LOCAL_IMPORT_RE.exec(code) Note over L: Backtracking explosion\nhandler exceeds 10s timeout\n→ falls back to "allow" L-->>H: timeout H-->>CC: allow (silently degraded)[\s\S]{0,N}?keeps worst-case parse linear in line length.import ... from "..."statements are short by design; a small ceiling (e.g. 2 KB) is well above realistic specifiers.Proposed enhancement
__tests__/hooks/loader-utils.test.ts:'and"characters returns within 50 ms.Acceptance criteria
LOCAL_IMPORT_REandLOCAL_REQUIRE_RE.hookLogWarnso power users see why their giant generated file was skipped.expect(duration).toBeLessThan(50)).CHANGELOG.mdentry under Unreleased → Fixes.Severity
Low — exploitable only by self-authored policy files (which already run arbitrary code), but cheap to fix and removes a foot-gun on copy-pasted/minified files.