Summary
Layer.prototype in lib/layer.js builds the list of route parameter keys for RegExp routes by scanning the regex source with:
const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g
This scan does not skip escaped literal parentheses (\( / \)). Every ( in the source (other than (?: non-capturing groups) is counted as a capture group, even when the backslash makes it a literal character. The generated keys list therefore becomes misaligned with the real capture groups returned by RegExp#exec, so req.params is populated with the wrong indices.
Repro
const express = require('express')
const app = express()
// escaped literal paren group ahead of two named groups
app.get(
/\/tenant\(v\d+\)-(?<tenant>[a-z]+)-(?<user>[a-z]+)$/,
(req, res) => res.json(req.params),
)
app.listen(3000)
GET /tenant(v1)-acme-boss
Actual : { "tenant": "boss" } // user missing, tenant got user's value
Expected : { "tenant": "acme", "user": "boss" }
The route still matches and no error is thrown; only the named parameter identity is corrupted.
Why
Real exec() capture groups: (?<tenant>...), (?<user>...) -> match.length === 3.
Scanner-produced keys: \( (counted), tenant, user -> 3 keys, but the key for capture index 1 is {name: 0} and index 2 is tenant, leaving user unmapped. params.tenant receives match[2], params.user is never set.
Suggested fix direction
Only scan parens that are not backslash-escaped, e.g.:
const MATCHING_GROUP_REGEXP = /(?<!\\)\((?:\?<(.*?)>)?(?!\?)/g
Scope note
Deterministic and developer-observable; the pattern is authored by the developer, not attacker-controlled, so this is a correctness issue rather than a security boundary (matches the maintainer classification of the related advisory GHSA-h9x4-4pf4-ffx8). Happy to adjust the fix or add a regression test.
Summary
Layer.prototypeinlib/layer.jsbuilds the list of route parameter keys for RegExp routes by scanning the regex source with:This scan does not skip escaped literal parentheses (
\(/\)). Every(in the source (other than(?:non-capturing groups) is counted as a capture group, even when the backslash makes it a literal character. The generatedkeyslist therefore becomes misaligned with the real capture groups returned byRegExp#exec, soreq.paramsis populated with the wrong indices.Repro
The route still matches and no error is thrown; only the named parameter identity is corrupted.
Why
Real
exec()capture groups:(?<tenant>...),(?<user>...)->match.length === 3.Scanner-produced keys:
\((counted),tenant,user-> 3 keys, but the key for capture index 1 is{name: 0}and index 2 istenant, leavinguserunmapped.params.tenantreceivesmatch[2],params.useris never set.Suggested fix direction
Only scan parens that are not backslash-escaped, e.g.:
Scope note
Deterministic and developer-observable; the pattern is authored by the developer, not attacker-controlled, so this is a correctness issue rather than a security boundary (matches the maintainer classification of the related advisory GHSA-h9x4-4pf4-ffx8). Happy to adjust the fix or add a regression test.