Repository navigation
JS: Why does SanitizingRegExpTest treat /^...$/m as a whole-string check? #22845
Description
Description of the issue
JavaScript taint-tracking queries treat a test against a regular expression anchored with ^ and $ as a check of the whole string, also when the regular expression has the m flag. I'm wondering whether this is intended, because with m, ^ and $ also match at line boundaries, so any string that contains one matching line passes the check: /^[\w.-]+$/m.test("x\ntouch /tmp/marker") is true.
Affected sanitizer
SanitizingRegExpTest, which is "considered to be a sanitizer for all configurations", uses RegExp::isGenericRegExpSanitizer for re.test(x), re.exec(x), x.match(re) and x.matchAll(re). For the true outcome, that predicate only requires isFullyAnchoredTerm, which looks for a leading RegExpCaret and a trailing RegExpDollar. The flags of the regular expression are not consulted, although the same module has isMultiline and maybeMultiline.
Steps to reproduce
const cp = require("child_process");
const app = require("express")();
app.get("/a", (req, res) => {
const dir = req.query.dir;
if (/^[\w.-]+$/m.test(dir)) {
cp.exec("ls " + dir); // not reported
}
res.send("ok");
});
app.get("/b", (req, res) => {
const dir = req.query.dir;
if (/^[\w.-]+$/.test(dir)) {
cp.exec("ls " + dir); // not reported (correct: no newline can pass)
}
res.send("ok");
});
app.get("/c", (req, res) => {
const dir = req.query.dir;
cp.exec("ls " + dir); // reported
res.send("ok");
});
app.get("/d", (req, res) => {
const name = req.query.name;
if (/^\w+$/m.test(name)) {
res.send("<p>Hello " + name + "</p>"); // not reported
} else {
res.send("bad name");
}
});
app.get("/e", (req, res) => {
res.send("<p>Hello " + req.query.name + "</p>"); // reported
});
app.listen(3000);With javascript-security-extended.qls, only /c (js/command-line-injection) and /e (js/reflected-xss) are reported. /a and /d are not, although at runtime their checks do not restrict the input:
dir = "x\ntouch /tmp/marker"(sent as?dir=x%0Atouch%20/tmp/marker) passes the check in/a, andcp.exec("ls x\ntouch /tmp/marker")runstouch /tmp/markeras a second command. The file is created.name = "x\n<script>alert(1)</script>"passes the check in/d, and the script element is sent in the response.
Without the m flag (/b), the check does restrict the input, so not reporting it is right.
The same mistake has led to real vulnerabilities, e.g. GHSA-3px3-54cx-rmw9 (CVE-2026-83609, @xmldom/xmldom 0.9.0–0.9.11), where an anchored validation regular expression was compiled with the m flag and a name with an embedded line terminator passed the check.
Question
Is it intended that the m flag is ignored here? Would it be reasonable to stop treating an anchored regular expression as a whole-string check when it has the m flag?
Environment
- CodeQL CLI 2.27.2 (
codeql/javascript-queries2.4.7) - CodeQL repository commit for the links above:
781e068896eb7a1a104f1378e8f3f68a1074a015(the files are unchanged since 2.27.2) - Node.js v24.18.0 on Linux/amd64
Activity
[-]JS: Why are anchored regexp checks with the `m` flag treated as complete sanitizer guards?[/-][+]JS: Why does SanitizingRegExpTest treat /^...$/m as a whole-string check?[/+]