Skip to content

JS: Why does SanitizingRegExpTest treat /^...$/m as a whole-string check? #22845

Open

Description

@wildoranges

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, and cp.exec("ls x\ntouch /tmp/marker") runs touch /tmp/marker as 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-queries 2.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

  1. changed the title [-]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?[/+] on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions