Context Mixing: Exploiting @ExecutionContext in GraphQL Modules

Context Mixing: Exploiting @ExecutionContext in GraphQL Modules
GraphQL has matured into the backbone of federated and modularized data layers in modern web architecture. But as abstraction layers grow more sophisticated, so do the attack surfaces hiding beneath them. One such risk lives at the intersection of Dependency Injection, asynchronous JavaScript, and shared singleton state — and it’s more dangerous than most teams realize.
This post breaks down a critical class of vulnerability in the GraphQL Modules framework, explains the real-world mechanics behind it, ties it to a recently patched Node.js CVE that proves the threat is not theoretical, and gives you concrete steps to protect your API.
What Is GraphQL Modules?
GraphQL Modules is a toolkit maintained by The Guild that enables teams to build reusable, maintainable, and testable modules for GraphQL servers. At its core is a powerful Dependency Injection system that lets developers define “Providers” — services responsible for business logic, data fetching, and authorization.
One of its most useful features is the @ExecutionContext decorator.
The Role of @ExecutionContext
In a standard GraphQL execution, a context object is threaded through every resolver. It typically carries the current user’s session, authentication tokens, and database connections.
The tricky part: when you’re working with Singleton providers — services instantiated once for the lifetime of the application — you can’t directly access per-request context. The @ExecutionContext decorator was designed to bridge that gap. It lets a Singleton service dynamically access the context of the current execution, typically by tapping into Node.js’s asynchronous resource tracking.
As the official GraphQL Modules docs describe it:
“Thanks to
@ExecutionContextdecorator, every Singleton provider gets access to the GraphQL Context and the Operation scoped Injector.”
This sounds clean. In practice, under concurrent load, it is a loaded gun.
The Vulnerability: Context Mixing Under Concurrent Load
The vulnerability is a race condition (CWE-362) that emerges when multiple parallel requests hit a Singleton service using @ExecutionContext.
Root Cause: State Pollution in the DI Container
The problem stems from improper isolation of the execution context inside the DI container. When two or more requests are processed simultaneously, the framework may fail to properly synchronize the injection of context into the shared service instance.
Under heavy load, consider this scenario:
- User A (an Administrator) and User B (a regular user) send requests at nearly the same millisecond.
- Both requests invoke a Singleton service that uses
@ExecutionContext. - The framework assigns User A’s context to the Singleton.
- User A’s resolver hits an
await— a database call, an API request, anything asynchronous. - While User A is suspended at the
await, Node.js’s Event Loop picks up User B’s request. - Because the Singleton instance is shared, its internal
contextreference is now overwritten — or worse, User A’s elevated context bleeds into User B’s execution path.
This is a classic Time-of-Check Time-of-Use (TOCTOU) bug. The service reads the context at one point in time, but by the time it acts on that context for a sensitive operation, the value has been swapped by a concurrent request.
Why Node.js Makes This Worse
Node.js is single-threaded but handles concurrency through the Event Loop and asynchronous callbacks. The async_hooks module (and its modern successor AsyncLocalStorage) are designed to track execution context across async boundaries. However, any await creates a “suspension point” — a window where the event loop can process another request and overwrite shared references in a Singleton.
The GraphQL Modules GitHub issues have documented real production cases where accidentally injecting a session-scoped service into an application-scoped (Singleton) provider caused memory leaks and state cross-contamination. Context mixing is the security-impacting form of the exact same architectural mistake.
Real-World Context: CVE-2025-59466
This is not a purely theoretical class of bug. On January 13, 2026, the Node.js security team patched CVE-2025-59466 — a critical flaw in async_hooks that had lurked in the codebase for nine years.
The vulnerability showed that when a stack overflow occurs inside an async_hooks callback (triggered by deeply nested recursive input), the runtime would invoke a fatal error handler and terminate the entire server process — with no opportunity for userland code to catch the error. Every in-flight request was dropped.
More relevantly for context mixing: this CVE revealed that async_hooks-based context tracking is fragile under adversarial conditions. Anything that disrupts the normal async execution chain — extreme nesting, recursive resolution, or high-concurrency hammering — can corrupt the context propagation that frameworks like GraphQL Modules rely on.
Affected Node.js versions: 8.x through 18.x (all end-of-life and unpatched). Patched versions: 20.20.0, 22.22.0, 24.13.0, and 25.3.0.
Additionally, the Node.js team has officially marked the lower-level async_hooks API as Stability 1 - Experimental, strongly recommending migration to AsyncLocalStorage. In Node.js 24+, AsyncLocalStorage was reimplemented on top of V8’s AsyncContextFrame, removing its dependency on async_hooks.createHook() entirely and eliminating this entire class of fragility.
The GraphQL Modules context mixing vulnerability lives in this same neighborhood: a framework-level abstraction that trusts async_hooks-based context isolation to hold under concurrent load.
Exploit Scenario: Privilege Escalation in a Financial Application
Imagine a financial application where an administrator is batch-processing payroll while a malicious user is simply refreshing their profile page.
The Setup
The application uses a FinanceService to authorize transactions. For performance, it is declared as a Singleton and uses @ExecutionContext to pull the caller’s token:
import { Injectable, ExecutionContext, Scope } from 'graphql-modules';
@Injectable({ scope: Scope.Singleton })
class FinanceService {
@ExecutionContext()
private context: ExecutionContext;
async transferFunds(amount: number) {
// This is the point of failure under concurrent load
const token = this.context.injector.get(AUTH_TOKEN);
return await bankApi.post('/transfer', { amount }, {
headers: { Authorization: `Bearer ${token}` }
});
}
}
The Attack Vector
The attacker doesn’t need advanced skills. A simple script — or even Burp Suite’s Intruder — sending a flood of requests during a known admin session is sufficient. The goal is purely timing.
The Collision
- User A (Admin) triggers a sensitive
transferFundsmutation. - The framework sets
@ExecutionContextto the Admin’s context inside the Singleton. - User A’s resolver hits the
await bankApi.post(...)call and suspends. - The Event Loop picks up User B’s request.
- Due to the race condition in the Singleton’s context reference, User B’s execution either overwrites the context — or inherits User A’s admin token.
- User B’s request now executes
transferFundscarrying the Admin’sAuthorizationheader. - RBAC is bypassed entirely. Audit logs show the transaction as if it came from the Admin.
Proof of Concept Pattern
Security researchers validate this class of bug using deferred promise injection — intentionally pausing one request’s execution at an await boundary, then racing a second request to demonstrate that the Singleton’s context reference changes. Under a timing-based test harness, the context swap is reliably reproducible at high concurrency.
Impact Assessment
The severity of this class of vulnerability is HIGH. The impact spans both Confidentiality and Integrity.
| Risk Area | Potential Impact |
|---|---|
| Unauthorized Data Access | Users can view PII, financial records, or private messages belonging to concurrent users |
| Privilege Escalation | A low-privileged user can perform administrative actions under another user’s identity |
| Account Takeover | Leaked session tokens can persist access long after the race window closes |
| Audit Trail Corruption | Malicious mutations appear in logs as if performed by the victim user |
Remediation and Best Practices
1. Update GraphQL Modules
If you are on a vulnerable version, patch immediately.
npm install graphql-modules@latest
# or
yarn add graphql-modules@latest
The fix involves re-architecting how @ExecutionContext tracks the internal injector, ensuring that context references are never shared across async boundaries.
Fixed versions:
- graphql-modules ≥ 2.4.1
- graphql-modules ≥ 3.1.1
- @envelop/graphql-modules ≥ 9.1.0
2. Upgrade Node.js
Upgrade to one of the patched Node.js versions: 20.20.0, 22.22.0, 24.13.0, or 25.3.0. These releases address the underlying async_hooks fragility. If you can move to Node.js 24+, AsyncLocalStorage no longer relies on async_hooks.createHook() at all.
3. Prefer Operation Scope Over Singleton for Auth-Sensitive Services
This is the most structurally sound mitigation. The GraphQL Modules documentation is clear on this point: Operation scope does not overlap across requests. For three concurrent requests, three isolated service instances are created — one per request.
import { Injectable, Scope, createModule } from 'graphql-modules';
@Injectable({ scope: Scope.Operation }) // Per-request, fully isolated
class FinanceService {
constructor(private authContext: AuthContext) {}
async transferFunds(amount: number) {
const token = this.authContext.token; // Safe: bound to this request only
return await bankApi.post('/transfer', { amount }, {
headers: { Authorization: `Bearer ${token}` }
});
}
}
Yes, there is a minor performance cost — services are instantiated per operation rather than reused. For services that handle authentication or authorization decisions, this is the correct trade-off. Singletons should be reserved for truly stateless, request-agnostic services: configuration readers, caches with their own internal isolation, connection pool managers, and so on.
4. Push Auth Into Global Middleware, Not Service Internals
Rather than having services “pull” user context via a decorator (which relies on the runtime correctly wiring that context), validate and attach authorization data before the execution reaches the DI container. An Envelop plugin or Express middleware is the right place for this:
const useAuthentication = () => ({
onExecute({ args }) {
const token = args.contextValue.token;
if (!token || !isValid(token)) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
}
});
This “push” model eliminates the possibility of context mixing at the decorator level entirely.
5. Implement Request Correlation IDs for Detection
Even if you cannot immediately patch, you can detect context mixing in your logs. Assign a unique Request-ID to every incoming request and include it in all log lines throughout that request’s lifecycle. If your logs show a single Request-ID associated with more than one User-ID, you have evidence of context cross-contamination.
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
res.setHeader('X-Request-ID', req.requestId);
next();
});
The Broader Lesson: Shared State in Async Environments Is Dangerous
CVE-2025-59466 and the GraphQL Modules context mixing risk both point to the same underlying truth: async context tracking is hard to get right, and “magical” abstractions built on top of it inherit that fragility.
Node.js’s own documentation now marks the raw async_hooks API as experimental and recommends against it. The Node.js team strongly discourages use of the async_hooks API, noting that async context tracking use cases are better served by the stable AsyncLocalStorage API. Frameworks that were built on top of async_hooks before AsyncLocalStorage matured are carrying technical debt that can manifest as security vulnerabilities under adversarial concurrency.
The rule for developers is simple and absolute: state that belongs to a user must never live in a Singleton.
If you’re using @ExecutionContext today, audit every Singleton provider that uses it. Ask yourself: what happens if this context reference is wrong? If the answer involves auth tokens, permission checks, or sensitive data — that service should not be a Singleton.
Going Further
The context mixing flaw is one of several emerging threats in the GraphQL ecosystem. Other areas worth auditing in your own stack include query complexity attacks (unbounded resolver depth leading to DoS), batching exploits via GraphQL’s native aliasing, introspection exposure in production, and the broader class of TOCTOU bugs in any DI framework that bridges singleton and request-scoped state across an async boundary.
Your resolvers are only as secure as the context they trust.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.