Skip to content

Commit 54c9286

Browse files
committed
fix(formula): harden D4 role-exists regex against ReDoS
CodeQL flagged js/polynomial-redos (high) on ROLE_EXISTS_RE: the unbounded greedy [^,]* + lazy [^)]*? scanners backtrack O(n^2) on adversarial input like repeated 'user.roles.exists('. Bound both quantifiers ({0,64}/{0,128} — CEL exists bodies are tiny) and exclude '=' from the pre-operator class so the run is linear and unambiguous. Capture groups unchanged; 206 formula tests pass; a 720k-char pathological probe now returns in ~14ms.
1 parent 6e9acf3 commit 54c9286

1 file changed

Lines changed: 6 additions & 1 deletion

File tree

packages/formula/src/validate.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,12 @@ function levenshtein(a: string, b: string): number {
159159
// `.role`). Matched names are validated against the closed role catalog.
160160
const ROLE_IN_RE = /(['"])([a-z0-9_]+)\1\s+in\s+(?:current_user|user|ctx\.user)\.roles\b/g;
161161
const ROLE_CONTAINS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*contains\(\s*(['"])([a-z0-9_]+)\1\s*\)/g;
162-
const ROLE_EXISTS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*exists\s*\([^,]*,\s*[^)]*?==\s*(['"])([a-z0-9_]+)\1/g;
162+
// Bounded quantifiers ({0,N}, not * / *?) keep this linear: a CEL `exists`
163+
// body is tiny in practice, and unbounded greedy/lazy scanners here backtrack
164+
// polynomially (O(n^2)) on adversarial input like repeated `user.roles.exists(`
165+
// (ADR-0068 D4 ReDoS hardening). The pre-`==` class excludes `=` so the bounded
166+
// run stops cleanly before the operator without a lazy quantifier.
167+
const ROLE_EXISTS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*exists\s*\([^,)]{0,64},[^)=]{0,128}==\s*(['"])([a-z0-9_]+)\1/g;
163168
const ROLE_EQ_RE = /(?:current_user|user|ctx\.user)\.role\s*==\s*(['"])([a-z0-9_]+)\1/g;
164169

165170
/**

0 commit comments

Comments
 (0)