Skip to content

Escaped literal parenthesis miscounts RegExp capture groups during parameter extraction #204

Description

@sl4x0

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions