Context Mixing: Ausnutzen von @ExecutionContext in GraphQL-Modulen

GraphQL hat sich zur Rückgrat federated und modularisierter Datenlagen in moderner Web-Architektur entwickelt. Doch mit zunehmender Abstraktion steigen auch die Angriffsflächen. Eine solche Gefahr besteht an der Schnittstelle von Dependency Injection, asynchronem JavaScript und gemeinsam genutztem Singleton-Zustand — und sie ist gefährlicher, als die meisten Teams vermuten.
Dieser Beitrag analysiert eine kritische Klasse von Schwachstellen im Framework der GraphQL Modules, erklärt die realen Mechanismen dahinter, verbindet sie mit einem kürzlich gepatchten Node.js CVE, das die Bedrohung beweist, und gibt konkrete Schritte zum Schutz Ihrer API.
Was sind GraphQL Modules?
GraphQL Modules ist ein Toolkit, das von The Guild gepflegt wird und Teams ermöglicht, wiederverwendbare, wartbare und testbare Module für GraphQL-Server zu erstellen. Im Kern steht ein leistungsstarkes Dependency Injection-System, das Entwicklern erlaubt, “Providers” zu definieren — Dienste, die für Geschäftslogik, Datenabruf und Autorisierung verantwortlich sind.
Eines seiner nützlichsten Features ist der @ExecutionContext-Decorator.
Die Rolle von @ExecutionContext
Bei einer standardmäßigen GraphQL-Ausführung wird ein context-Objekt durch jeden Resolver gereicht. Es enthält typischerweise die Sitzung des aktuellen Nutzers, Authentifizierungstoken und Datenbankverbindungen.
Das Knifflige: Wenn Sie mit Singleton-Providern arbeiten — Diensten, die einmal für die Lebensdauer der Anwendung instanziiert werden — können Sie nicht direkt auf den Request-spezifischen Kontext zugreifen. Der @ExecutionContext-Decorator wurde entwickelt, um diese Lücke zu schließen. Er ermöglicht einem Singleton-Dienst, dynamisch auf den Kontext der aktuellen Ausführung zuzugreifen, meist durch Nutzung der asynchronen Ressourcenverfolgung von Node.js.
Wie die offiziellen GraphQL Modules-Dokumentationen beschreiben:
e “Dank des @ExecutionContext-Decorators erhält jeder Singleton-Provider Zugriff auf den GraphQL-Kontext und den operationenspezifischen Injector.”
Das klingt sauber. In der Praxis, bei gleichzeitiger Belastung, ist es eine gefährliche Waffe.
Die Schwachstelle: Context Mixing bei gleichzeitiger Belastung
Die Schwachstelle ist ein Race Condition (CWE-362), das auftritt, wenn mehrere parallele Requests einen Singleton-Dienst mit @ExecutionContext ansprechen.
Ursachen: Zustandsverschmutzung im DI-Container
Das Problem liegt in unzureichender Isolierung des Ausführungskontexts im DI-Container. Wenn zwei oder mehr Requests gleichzeitig verarbeitet werden, kann es passieren, dass die Injection des Kontexts in die gemeinsame Service-Instanz nicht korrekt synchronisiert wird.
Unter hoher Last: Stellen Sie sich folgendes Szenario vor:
- Benutzer A (Administrator) und Benutzer B (normaler Nutzer) senden nahezu gleichzeitig Anfragen.
- Beide rufen einen Singleton-Service auf, der
@ExecutionContextnutzt. - Das Framework weist den Kontext von Benutzer A dem Singleton zu.
- Der Resolver von Benutzer A erreicht einen
await— eine Datenbankabfrage, API-Anfrage oder eine andere asynchrone Operation. - Während Benutzer A bei
awaitpausiert, verarbeitet die Event Loop Node.jss die Anfrage von Benutzer B. - Da die Singleton-Instanz geteilt ist, wird die interne
context-Referenz überschrieben — oder schlimmer noch, die erhöhte Berechtigungskontext von Benutzer A sickert in den Ausführungsweg von Benutzer B.
Dies ist ein klassischer Time-of-Check Time-of-Use (TOCTOU)-Bug. Der Dienst liest den Kontext zu einem Zeitpunkt, aber wenn er auf diesen Kontext für eine sensible Operation zugreift, wurde der Wert durch eine gleichzeitige Anfrage überschrieben.
Warum Node.js das verschärft
Node.js ist single-threaded, handhabt aber Parallelität durch die Event Loop und asynchrone Callbacks. Das async_hooks-Modul (und sein moderner Nachfolger AsyncLocalStorage) sind dafür gedacht, den Ausführungskontext über asynchrone Grenzen hinweg zu verfolgen. Doch jedes await schafft einen “Suspension Point” — ein Fenster, in dem die Event Loop eine andere Anfrage verarbeiten und geteilte Referenzen in einem Singleton überschreiben kann.
Die GitHub-Issues zu GraphQL Modules dokumentieren reale Fälle, in denen das versehentliche Injizieren eines session-spezifischen Dienstes in einen anwendungsweiten (Singleton) Provider zu Speicherlecks und Zustandskontamination führte. Context Mixing ist die sicherheitsrelevante Variante desselben architektonischen Fehlers.
Reale Welt: CVE-2025-59466
Dies ist kein rein theoretischer Bug. Am 13. Januar 2026 patchte das Node.js-Sicherheitsteam CVE-2025-59466 — eine kritische Schwachstelle in async_hooks, die neun Jahre im Code schlummerte.
Die Schwachstelle zeigte, dass bei einem Stack-Overflow innerhalb eines async_hooks-Callbacks (bei tief verschachtelter rekursiver Eingabe) die Laufzeit einen fatalen Fehler-Handler aufruft und den gesamten Serverprozess beendet — ohne Möglichkeit für Nutzerland-Code, den Fehler abzufangen. Jede laufende Anfrage wurde verworfen.
Relevanter für Context Mixing: Diese CVE zeigte, dass die Kontextverfolgung mit async_hooks unter feindlichen Bedingungen fragil ist. Alles, was die normale asynchrone Ausführungskette stört — extreme Verschachtelung, rekursive Auflösung oder hohe Parallelität — kann die Kontextübertragung, auf die Frameworks wie GraphQL Modules angewiesen sind, beschädigen.
Betroffene Node.js-Versionen: 8.x bis 18.x (alle End-of-Life und ungepatcht). Gepatchte Versionen: 20.20.0, 22.22.0, 24.13.0 und 25.3.0.
Zusätzlich hat das Node.js-Team die niedrigstufige async_hooks-API offiziell als Stabilität 1 - Experimentell markiert und empfiehlt die Migration zu AsyncLocalStorage. Ab Node.js 24+ wurde AsyncLocalStorage auf V8’s AsyncContextFrame neu implementiert, wodurch die Abhängigkeit von async_hooks.createHook() entfällt und diese Fragilität beseitigt wird.
Das Context Mixing in GraphQL Modules lebt in diesem selben Bereich: eine Framework-Abstraktion, die auf async_hooks-basierte Kontextisolierung vertraut, um unter gleichzeitiger Belastung standzuhalten.
Exploit-Szenario: Privilegieneskalation in einer Finanzanwendung
Stellen Sie sich eine Finanzanwendung vor, in der ein Administrator die Gehaltsabrechnung batchweise verarbeitet, während ein böswilliger Nutzer nur seine Profilseite aktualisiert.
Das Setup
Die Anwendung nutzt einen FinanceService, um Transaktionen zu autorisieren. Für die Performance ist dieser als Singleton deklariert und verwendet @ExecutionContext, um das Token des Anrufers zu ziehen:
import { Injectable, ExecutionContext, Scope } from 'graphql-modules';
@Injectable({ scope: Scope.Singleton })
class FinanceService {
@ExecutionContext()
private context: ExecutionContext;
async transferFunds(amount: number) {
// Hier liegt die Schwachstelle bei gleichzeitiger Belastung
const token = this.context.injector.get(AUTH_TOKEN);
return await bankApi.post('/transfer', { amount }, {
headers: { Authorization: `Bearer ${token}` }
});
}
}
Der Angriffsvektor
Der Angreifer braucht keine fortgeschrittenen Fähigkeiten. Ein einfaches Skript — oder sogar Burp Suite’s Intruder —, das eine Flut von Requests während einer bekannten Admin-Session sendet, reicht aus. Ziel ist nur das Timing.
Die Kollision
- Benutzer A (Admin) löst eine sensible
transferFunds-Mutation aus. - Das Framework setzt
@ExecutionContextauf den Kontext des Admins im Singleton. - Der Resolver von Benutzer A erreicht den
await bankApi.post(...)-Aufruf und pausiert. - Die Event Loop verarbeitet die Anfrage von Benutzer B.
- Aufgrund des Race Conditions in der Kontextreferenz überschreibt die Ausführung von Benutzer B entweder den Kontext — oder erbt den Admin-Token von Benutzer A.
- Die Anfrage von Benutzer B führt
transferFundsmit demAuthorization-Header des Admins aus. - RBAC wird vollständig umgangen. Die Logs zeigen die Transaktion, als käme sie vom Admin.
Proof-of-Concept-Muster
Sicherheitsforscher validieren diese Bug-Klasse mit verzögerter Promise-Injektion — absichtliches Pausieren der Ausführung einer Anfrage bei einem await-Grenzpunkt, dann ein Rennen gegen eine zweite Anfrage, um zu demonstrieren, dass die Kontextreferenz im Singleton sich ändert. Unter Timing-Tests ist der Kontextwechsel bei hoher Parallelität zuverlässig reproduzierbar.
Auswirkungen
Die Schwere dieser Schwachstelle ist HOCH. Die Auswirkungen betreffen sowohl Vertraulichkeit als auch Integrität.
| Risiko-Bereich | Potenzielle Auswirkungen |
|---|---|
| Unbefugter Datenzugriff | Nutzer können PII, Finanzdaten oder private Nachrichten anderer Nutzer einsehen |
| Privilegieneskalation | Ein Nutzer mit niedrigen Rechten kann administrative Aktionen im Namen eines anderen durchführen |
| Kontenübernahme | Leakte Sitzungstoken können Zugriff auch nach Ende des Race-Fensters gewähren |
| Manipulation der Audit-Logs | Malicious mutations erscheinen in den Logs, als würden sie vom Opfer stammen |
Behebung und Best Practices
1. Aktualisieren Sie GraphQL Modules
Wenn Sie eine anfällige Version verwenden, patchen Sie sofort.
npm install graphql-modules@latest
# oder
yarn add graphql-modules@latest
Der Fix beinhaltet eine Neugestaltung, wie @ExecutionContext den internen Injector verfolgt, um sicherzustellen, dass Kontextreferenzen niemals über asynchrone Grenzen hinweg geteilt werden.
Gepatchte Versionen:
- graphql-modules ≥ 2.4.1
- graphql-modules ≥ 3.1.1
- @envelop/graphql-modules ≥ 9.1.0
2. Node.js aktualisieren
Auf eine der gepatchten Node.js-Versionen upgraden: 20.20.0, 22.22.0, 24.13.0 oder 25.3.0. Diese Versionen beheben die zugrunde liegende async_hooks-Fragilität. Wenn Sie auf Node.js 24+ umsteigen, basiert AsyncLocalStorage nicht mehr auf async_hooks.createHook() und ist somit weniger anfällig.
3. Operation Scope statt Singleton für auth-sensitive Dienste bevorzugen
Dies ist die robusteste Lösung. Die GraphQL Modules-Dokumentation macht klar: Der Operation Scope überschneidet sich nicht zwischen Requests. Für drei parallele Requests werden drei isolierte Service-Instanzen erstellt — eine pro Request.
import { Injectable, Scope, createModule } from 'graphql-modules';
@Injectable({ scope: Scope.Operation }) // pro Request, vollständig isoliert
class FinanceService {
constructor(private authContext: AuthContext) {}
async transferFunds(amount: number) {
const token = this.authContext.token; // Sicher: nur an diesen Request gebunden
return await bankApi.post('/transfer', { amount }, {
headers: { Authorization: `Bearer ${token}` }
});
}
}
Ja, das hat einen kleinen Performance-Trade-off — Dienste werden pro Operation instanziiert, nicht wiederverwendet. Für Dienste, die Authentifizierung oder Berechtigungsentscheidungen treffen, ist das die richtige Wahl. Singletons sollten nur für wirklich stateless, request-unabhängige Dienste verwendet werden: Konfigurationsleser, Caches mit eigener Isolierung, Connection-Pool-Manager usw.
4. Authentifizierung in globale Middleware verschieben, nicht in Service-Interna
Statt, dass Dienste “User-Kontext” via Decorator “ziehen” (was auf die korrekte Verkabelung im Runtime angewiesen ist), validieren und hängen Sie Autorisierungsdaten vor der Ausführung an den DI-Container an. Ein Envelop-Plugin oder Express-Middleware ist dafür geeignet:
const useAuthentication = () => ({
onExecute({ args }) {
const token = args.contextValue.token;
if (!token || !isValid(token)) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' }
});
}
}
});
Dieses “Push”-Modell eliminiert die Möglichkeit des Context-Mixings auf Decorator-Ebene vollständig.
5. Request-Correlation-IDs für Erkennung implementieren
Selbst wenn Sie nicht sofort patchen können, können Sie Context-Mixing in Ihren Logs erkennen. Weisen Sie jeder eingehenden Anfrage eine eindeutige Request-ID zu und fügen Sie diese in alle Log-Zeilen während des Request-Lebenszyklus ein. Wenn Ihre Logs eine Request-ID mit mehreren User-IDs zeigen, liegt ein Beweis für Cross-Contamination vor.
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
res.setHeader('X-Request-ID', req.requestId);
next();
});
Die größere Lektion: Gemeinsamer Zustand in asynchronen Umgebungen ist gefährlich
CVE-2025-59466 und das Risiko des Context Mixing in GraphQL Modules zeigen eine gemeinsame Wahrheit: async-Kontextverfolgung ist schwer richtig umzusetzen, und “magische” Abstraktionen darauf bauen, erben diese Fragilität.
Die Node.js-Dokumentation markiert das rohe async_hooks-API jetzt als experimentell und empfiehlt die Nutzung von AsyncLocalStorage. Das Node.js-Team rät dringend, async_hooks zu meiden. Frameworks, die vor der Reife von AsyncLocalStorage darauf gebaut haben, tragen technischen Schuldenberg, der bei hoher Parallelität Sicherheitslücken offenbaren kann.
Die klare Devise: Zustand, der einem Nutzer gehört, darf niemals in einem Singleton leben.
Wenn Sie @ExecutionContext verwenden, prüfen Sie alle Singleton-Provider, die es nutzen. Fragen Sie sich: Was passiert, wenn dieser Kontext falsch referenziert wird? Wenn die Antwort Tokens, Berechtigungen oder sensible Daten betrifft — sollte dieser Service kein Singleton sein.
Weiterführende Schritte
Der Fehler des Context Mixing ist nur einer von mehreren aufkommenden Bedrohungen im GraphQL-Ökosystem. Weitere Bereiche, die Sie in Ihrer eigenen Stack prüfen sollten, sind Query-Complexity-Attacken (unbegrenzte Resolver-Tiefe, die zu DoS führen), Batching-Exploits durch native GraphQL-Aliasierung, Exposure von Introspection in Produktion und die breitere Klasse von TOCTOU-Bugs in jedem DI-Framework, das Singleton- und Request-spezifischen Zustand über eine async-Grenze verbindet.
Ihre Resolver sind nur so sicher wie der Kontext, dem sie vertrauen.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.