Reported: March 11, 2026 — CVE-2026-27193 NestJS
A recent NestJS release patches a prototype pollution flaw in the Reflector metadata utility that ships with @nestjs/core. Nest apps on affected pre-patch versions that pass user-controlled strings into SetMetadata or a custom decorator key are exposed. The fix replaces the internal plain-object metadata store with a Map, whose entries — per the MDN Map reference — live in an internal slot that is not reachable through Object.prototype, blocking writes to the prototype chain.
What does CVE-2026-27193 actually do to a NestJS app?
The flaw is a textbook prototype pollution. A crafted key reaches Reflect.defineMetadata (or a wrapper that forwards to it) and, because the underlying storage in the vulnerable build was a plain JavaScript object, assigning __proto__ or constructor.prototype on that object mutates every descendant of Object. Downstream code that reads flags like obj.isAdmin or obj.allowBypass then sees truthy values it never set. OWASP’s prototype pollution reference and CWE-1321 describe this attack class in depth.
The vulnerable pattern usually looks like this — a controller accepts a key/value pair and passes the key straight into Reflect’s metadata API, trusting that the decorator layer has already sanitized it:
import { Reflector } from '@nestjs/core';
import { Controller, Post, Body } from '@nestjs/common';
@Controller('tags')
export class TagController {
constructor(private reflector: Reflector) {}
@Post()
attach(@Body() body: { key: string; value: unknown }) {
Reflect.defineMetadata(body.key, body.value, TagController);
return { ok: true };
}
}
An attacker posts {"key":"__proto__","value":{"isAdmin":true}} and the next plain object constructed in that Node process carries isAdmin: true. Any authorization check that does if (user.isAdmin) without a hasOwnProperty guard misfires. Per the FIRST CVSS v3.1 specification, pollution in a framework where decorator metadata touches guards, interceptors, and validation pipes is categorically a network-reachable confidentiality and integrity issue; consult the NVD entry for the authoritative scoring vector.

The diagram traces the request flow end to end. User input hits a controller, the raw key is forwarded to Reflector.defineMetadata, the pre-patch Reflector writes to a plain-object map, and the polluted prototype chain leaks into guard evaluation. Two arrows show the patched path — input is routed into a Map<unknown, Map<string | symbol, unknown>> whose key space is isolated from Object.prototype entirely.
How does the Reflector metadata lookup work under the hood?
Nest’s Reflector is a thin wrapper around the reflect-metadata polyfill. Under the hood, reflect-metadata historically maintained a per-target object of string-keyed entries. In the patched release, the @nestjs/core Reflector injects a Map-based override for its own get/set paths. The NestJS execution-context docs describe the public Reflector surface and the createDecorator factory as the recommended entry points; the upstream reflect-metadata polyfill still backs third-party decorators, but Nest’s own methods no longer round-trip user-provided keys through the prototype chain.
The official docs page shown above describes the updated API surface. The NestJS documentation covers Reflector.createDecorator as the recommended way to declare typed metadata keys. Funnelling metadata through a decorator factory narrows the attack surface, because a plain string like "__proto__" cannot flow in without the factory wrapping it first, and the typed API gives TypeScript a chance to reject arbitrary string keys at compile time.
The patched implementation in the @nestjs/core Reflector service looks roughly like this:
export class Reflector {
private readonly store = new Map<unknown, Map<string | symbol, unknown>>();
public get<T>(metadataKey: ReflectableDecorator<T>, target: object): T | undefined {
const bucket = this.store.get(target);
if (!bucket) return undefined;
return bucket.get(metadataKey.KEY) as T | undefined;
}
public set<T>(metadataKey: ReflectableDecorator<T>, value: T, target: object): void {
let bucket = this.store.get(target);
if (!bucket) {
bucket = new Map();
this.store.set(target, bucket);
}
bucket.set(metadataKey.KEY, value);
}
}
Two properties matter. First, Map<unknown, ...> uses the target reference itself as the key, not a derived string — no prototype chain involved. Second, the inner bucket is also a Map keyed on string | symbol, which in V8 is a hash-table-backed structure with no link to Object.prototype. Assignments to __proto__ land in the Map’s internal table and stay there.
How do I upgrade and confirm the patch landed?
Run npm install @nestjs/core@latest @nestjs/common@latest, pinning to the latest patched release listed on the NestJS releases page. If you use pnpm or yarn workspaces, bump every package that depends on @nestjs/core transitively so you do not ship a mixed graph. Then confirm with npm ls @nestjs/core that only the patched version resolves — no deduped older copy hiding behind a sibling dependency.
npm install @nestjs/core@latest @nestjs/common@latest @nestjs/platform-express@latest
npm ls @nestjs/core
npm ls reflect-metadata
You want reflect-metadata at a current 0.2.x release too — not because it contains the Nest fix, but because older 0.1.x builds ship unguarded Object.prototype.hasOwnProperty checks that other libraries trip over. A clean install will pull the version declared in the peerDependencies of @nestjs/core, but workspaces sometimes pin an older one at the root, so verify with npm ls reflect-metadata and cross-check against the package listing on npm.
scalable Nest architecture goes into the specifics of this.

The terminal animation walks the upgrade end to end: npm install, npm audit fix, a quick Reflector check in a Node REPL (node -e "console.log(require('@nestjs/core').Reflector.prototype.get.toString().includes('Map'))"), and a curl against a controller that previously accepted a __proto__ payload. The clip ends with the server returning 400 from a class-validator rejection instead of silently polluting the prototype.
Drop this runtime check into a smoke test that runs on boot. It is noisy, but it catches the case where a Docker layer cache or a stale node_modules silently re-introduces the vulnerable Reflector:
import { Reflector } from '@nestjs/core';
const src = Reflector.prototype.get.toString();
if (!src.includes('Map') && !src.includes('#store')) {
throw new Error('Reflector is NOT the patched build');
}
What performance tradeoffs does the Map-based fix bring?
Swapping a plain object for a Map has a real cost on hot paths, but Nest’s Reflector is called during bootstrap (decorator registration) and during guard/interceptor dispatch. Dispatch is where the cost matters. In V8, Map lookups run through a hash-table probe rather than the hidden-class / inline-cache fast path that tight plain-object property reads enjoy — V8’s post on fast property access describes the cache mechanism — so you pay a small per-call overhead on every guard and interceptor dispatch. The absolute cost per call remains well below the noise of any I/O-bound handler, but it is measurable under a tight microbenchmark.

The chart compares three storage implementations: the old plain-object build, the patched Map-based build, and a WeakMap alternative. WeakMap reads are aggressively optimized in modern engines, but per MDN’s WeakMap reference, WeakMap keys must be objects (or non-registered symbols) — which rules out any string-keyed metadata the Reflector needs to carry. That constraint is the fundamental reason a plain Map is the natural fit: it accepts both symbol and string keys and keeps prototype-chain lookups out of the picture without imposing WeakMap’s object-only key requirement.
A related write-up: TypeScript runtime performance.
If you run a request budget that is allergic to this cost, you can cache Reflector lookups per-handler with @nestjs/common‘s SetMetadata factory — that collapses the runtime call into a constant-time property read on the handler function itself and skips Reflector.get entirely. For most apps this optimization is not worth the code churn.
What gotchas trip up the upgrade, and what should I verify before deploy?
Three failure modes show up in the first wave of upgrades. Walk these before you merge the bump.
Gotcha 1: TypeScript build fails with TS2345 after the bump
Error: error TS2345: Argument of type 'string' is not assignable to parameter of type 'ReflectableDecorator<unknown>'.
If you need more context, prototype-related runtime errors covers the same ground.
Root cause: under strict TypeScript settings you may need to migrate call sites to the typed decorator factory returned from Reflector.createDecorator, which the NestJS execution-context docs recommend for metadata declared in your own code.
Fix — migrate the call site to a typed decorator:
// before
const roles = this.reflector.get<string[]>('roles', context.getHandler());
// after
export const Roles = Reflector.createDecorator<string[]>();
const roles = this.reflector.get(Roles, context.getHandler());
Gotcha 2: Runtime throws “Cannot read properties of undefined (reading ‘KEY’)”
Error: TypeError: Cannot read properties of undefined (reading 'KEY') at Reflector.get.
Root cause: a guard or interceptor imports a decorator from a file that was not rebuilt after the upgrade, so it receives undefined where the patched build expects the decorator factory object returned by createDecorator. The compiled dist/ output is mixing old and new Reflector shapes.
Fix — clear the build cache and recompile from scratch:
rm -rf dist node_modules/.cache
npm run build
Gotcha 3: Jest tests fail with “Metadata not found for key”
Error: Error: Metadata not found for key Symbol(roles) on target class AuthGuard.
Root cause: Jest’s default module isolation creates a new Reflector instance per test file, and the patched Reflector uses per-instance Map storage instead of a global reflect-metadata store. Tests that defined decorators in one module but read them from a different instance now see empty metadata.
Fix — import the decorator from the same module in both setup and assertion, or pin module resolution in jest.config.ts:
# jest.config.ts
export default {
testEnvironment: 'node',
moduleDirectories: ['node_modules', '<rootDir>/src'],
resetModules: false,
};
Before hitting deploy
Run these checks on the release branch in this order, and do not ship until each one passes:
- Run
npm ls @nestjs/core @nestjs/common @nestjs/platform-expressand confirm every entry resolves to exactly the patched release — no deduped older build hiding in a transitive graph under a workspace sibling. - Grep the repo for
reflector.get('andSetMetadata('string literals and audit every key. Any key sourced fromreq.body,req.params, orreq.queryis exploitable and must be moved behind acreateDecoratorfactory. - Run the patched-build smoke test (
require('@nestjs/core').Reflector.prototype.get.toString().includes('Map')) inside the Docker image your CI builds, not just on your dev machine. A cached layer can pin an older Reflector even after the lockfile looks clean. - Re-run your integration suite with
NODE_OPTIONS='--frozen-intrinsics'. If prototype pollution still lands anywhere, frozen-intrinsics surfaces aTypeErrorinstead of a silent mutation — exactly the signal you want in CI rather than in prod. - Add a regression test that posts
{"key":"__proto__","value":{"admin":true}}to every endpoint that accepts free-form metadata, and assert the response is 400 or 422. Keep this test in the main suite permanently so a future refactor cannot reintroduce the pattern. - After the rollout, watch your observability pipeline for a sudden spike in
TypeErrorfromReflector.get. A mismatched transitive dep shows up there first, minutes before it shows up in user reports.
The upgrade itself is a single minor bump, but the blast radius of ignoring it is wide: prototype pollution in a framework’s decorator layer touches authorization, validation, and routing simultaneously. Pin the patched release today, add the runtime smoke check, and move on.
For a different angle, see modern Nest backend guide.
For a different angle, see tests worth writing.
- NestJS releases on GitHub — authoritative release notes and changelog.
- NVD entry for CVE-2026-27193 — the National Vulnerability Database listing with CVSS vector.
- NestJS docs: Reflection and metadata — official guidance on Reflector and createDecorator.
- reflect-metadata on GitHub — the underlying polyfill Nest builds on.
- @nestjs/core on GitHub — the package that contains the Reflector service.
- OWASP: Prototype pollution — attack-class reference and mitigation guidance.
