From 45f1c7df487285499adb716e6f28252ea1f58a13 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:46:41 +0000 Subject: [PATCH] feat: exclude simple word alternations from missing regexp anchor When a regex with misleading anchor precedence is used with .test() and the unanchored alternatives are simple words (no dots/TLD patterns), it is likely an intentional partial match for role/type checking rather than a hostname validation bypass. --- .../Security/CWE-020/MissingRegExpAnchor.ql | 25 ++++++++++++++----- .../tst-intentional-test.js | 10 ++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 javascript/ql/test/query-tests/Security/CWE-020/MissingRegExpAnchor/tst-intentional-test.js diff --git a/javascript/ql/src/Security/CWE-020/MissingRegExpAnchor.ql b/javascript/ql/src/Security/CWE-020/MissingRegExpAnchor.ql index 1057f9ccca50..3e18cfba3caf 100644 --- a/javascript/ql/src/Security/CWE-020/MissingRegExpAnchor.ql +++ b/javascript/ql/src/Security/CWE-020/MissingRegExpAnchor.ql @@ -47,10 +47,23 @@ import MissingRegExpAnchor::Make from DataFlow::Node nd, string msg where - isUnanchoredHostnameRegExp(nd, msg) - or - isSemiAnchoredHostnameRegExp(nd, msg) - or - hasMisleadingAnchorPrecedence(nd, msg) -// isLineAnchoredHostnameRegExp is not used here, as it is not relevant to JS. + ( + isUnanchoredHostnameRegExp(nd, msg) + or + isSemiAnchoredHostnameRegExp(nd, msg) + or + hasMisleadingAnchorPrecedence(nd, msg) + ) and + // Exclude patterns used with .test() where unanchored alternatives are simple words + // (no dots), indicating intentional partial matching for role/type checks, not URL validation + not exists(DataFlow::MethodCallNode testCall | + testCall.getMethodName() = "test" and + nd.(DataFlow::RegExpCreationNode).getARegExpObject().flowsTo(testCall.getReceiver()) and + forall(RegExpTerm alt | + alt = nd.(DataFlow::RegExpCreationNode).getRoot().(RegExpAlt).getAChild() and + not alt.getAChild*() instanceof RegExpAnchor + | + not alt.getAChild*().(RegExpConstant).getValue().matches("%.%") + ) + ) select nd, msg diff --git a/javascript/ql/test/query-tests/Security/CWE-020/MissingRegExpAnchor/tst-intentional-test.js b/javascript/ql/test/query-tests/Security/CWE-020/MissingRegExpAnchor/tst-intentional-test.js new file mode 100644 index 000000000000..86fead75de53 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-020/MissingRegExpAnchor/tst-intentional-test.js @@ -0,0 +1,10 @@ +(function() { + // GOOD: simple word alternation used with .test() for role checking + // These are intentional partial matches, not URL validation + var rolePattern = /^admin|user|guest/; + if (rolePattern.test(role)) { /* ... */ } + + // BAD: hostname pattern with misleading anchor precedence + var urlCheck = /^https?:\/\/good.com|https?:\/\/evil.com/; // $ Alert + if (urlCheck.test(url)) { /* ... */ } +});