GHSA-53WG-R69P-V3R7
Vulnerability from github – Published: 2026-01-16 21:09 – Updated: 2026-01-21 16:20Summary
Originally reported as an issue #2613 but should be elevated to a security issue as the ExecutionContext is often used to pass authentication tokens from incoming requests to services loading data from backend APIs.
Details
When 2 or more parallel requests are made which trigger the same service, the context of the requests is mixed up in the service when the context is injected via @ExecutionContext()
PoC
In a new project/folder, create and install the following package.json:
{
"name": "GHSA-53wg-r69p-v3r7",
"scripts": {
"test": "jest"
},
"dependencies": {
"graphql-modules": "2.4.0"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.28.6",
"babel-plugin-parameter-decorator": "^1.0.16",
"jest": "^29.7.0",
"reflect-metadata": "^0.2.2"
}
}
with:
npm i
configure babel.config.json using:
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"babel-plugin-parameter-decorator",
"@babel/plugin-proposal-class-properties"
]
}
then write the following test GHSA-53wg-r69p-v3r7.spec.ts:
require("reflect-metadata");
const {
createApplication,
createModule,
Injectable,
Scope,
ExecutionContext,
gql,
testkit,
} = require("graphql-modules");
test("accessing a singleton provider context during another asynchronous execution", async () => {
@Injectable({ scope: Scope.Singleton })
class IdentifierProvider {
@ExecutionContext()
context;
getId() {
return this.context.identifier;
}
}
const { promise: gettingBefore, resolve: gotBefore } = createDeferred();
const { promise: waitForGettingAfter, resolve: getAfter } = createDeferred();
const mod = createModule({
id: "mod",
providers: [IdentifierProvider],
typeDefs: gql`
type Query {
getAsyncIdentifiers: Identifiers!
}
type Identifiers {
before: String!
after: String!
}
`,
resolvers: {
Query: {
async getAsyncIdentifiers(_0, _1, context) {
const before = context.injector.get(IdentifierProvider).getId();
gotBefore();
await waitForGettingAfter;
const after = context.injector.get(IdentifierProvider).getId();
return { before, after };
},
},
},
});
const app = createApplication({
modules: [mod],
});
const document = gql`
{
getAsyncIdentifiers {
before
after
}
}
`;
const firstResult$ = testkit.execute(app, {
contextValue: {
identifier: "first",
},
document,
});
await gettingBefore;
const secondResult$ = testkit.execute(app, {
contextValue: {
identifier: "second",
},
document,
});
getAfter();
await expect(firstResult$).resolves.toEqual({
data: {
getAsyncIdentifiers: {
before: "first",
after: "first",
},
},
});
await expect(secondResult$).resolves.toEqual({
data: {
getAsyncIdentifiers: {
before: "second",
after: "second",
},
},
});
});
function createDeferred() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve,
reject,
};
}
and execute using:
npm test
Your project tree should look like this:
GHSA-53wg-r69p-v3r7
package.json
package-lock.json
babel.config.json
GHSA-53wg-r69p-v3r7.spec.js
Expected vs. Actual Outcome
- Expected - 1
+ Received + 1
Object {
"data": Object {
"getAsyncIdentifiers": Object {
- "after": "first",
+ "after": "second",
"before": "first",
},
},
}
Impact
Any application that uses services that inject the context using @ExecutionContext() from a singleton provider are at risk. The more traffic an application has, the higher the chance for parallel requests, the higher the risk.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "graphql-modules"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.1"
},
{
"fixed": "2.4.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "graphql-modules"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.1.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-23735"
],
"database_specific": {
"cwe_ids": [
"CWE-362"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-16T21:09:08Z",
"nvd_published_at": "2026-01-16T20:15:51Z",
"severity": "HIGH"
},
"details": "### Summary\nOriginally reported as an issue #2613 but should be elevated to a security issue as the ExecutionContext is often used to pass authentication tokens from incoming requests to services loading data from backend APIs.\n\n### Details\nWhen 2 or more parallel requests are made which trigger the same service, the context of the requests is mixed up in the service when the context is injected via `@ExecutionContext()`\n\n### PoC\n\nIn a new project/folder, create and install the following `package.json`:\n\n```json\n{\n \"name\": \"GHSA-53wg-r69p-v3r7\",\n \"scripts\": {\n \"test\": \"jest\"\n },\n \"dependencies\": {\n \"graphql-modules\": \"2.4.0\"\n },\n \"devDependencies\": {\n \"@babel/plugin-proposal-class-properties\": \"^7.18.6\",\n \"@babel/plugin-proposal-decorators\": \"^7.28.6\",\n \"babel-plugin-parameter-decorator\": \"^1.0.16\",\n \"jest\": \"^29.7.0\",\n \"reflect-metadata\": \"^0.2.2\"\n }\n}\n```\n\nwith:\n\n```\nnpm i\n```\n\nconfigure `babel.config.json` using:\n\n```json\n{\n \"plugins\": [\n [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }],\n \"babel-plugin-parameter-decorator\",\n \"@babel/plugin-proposal-class-properties\"\n ]\n}\n```\n\nthen write the following test `GHSA-53wg-r69p-v3r7.spec.ts`:\n\n```js\nrequire(\"reflect-metadata\");\nconst {\n createApplication,\n createModule,\n Injectable,\n Scope,\n ExecutionContext,\n gql,\n testkit,\n} = require(\"graphql-modules\");\n\ntest(\"accessing a singleton provider context during another asynchronous execution\", async () =\u003e {\n @Injectable({ scope: Scope.Singleton })\n class IdentifierProvider {\n @ExecutionContext()\n context;\n\n getId() {\n return this.context.identifier;\n }\n }\n\n const { promise: gettingBefore, resolve: gotBefore } = createDeferred();\n\n const { promise: waitForGettingAfter, resolve: getAfter } = createDeferred();\n\n const mod = createModule({\n id: \"mod\",\n providers: [IdentifierProvider],\n typeDefs: gql`\n type Query {\n getAsyncIdentifiers: Identifiers!\n }\n\n type Identifiers {\n before: String!\n after: String!\n }\n `,\n resolvers: {\n Query: {\n async getAsyncIdentifiers(_0, _1, context) {\n const before = context.injector.get(IdentifierProvider).getId();\n gotBefore();\n await waitForGettingAfter;\n const after = context.injector.get(IdentifierProvider).getId();\n return { before, after };\n },\n },\n },\n });\n\n const app = createApplication({\n modules: [mod],\n });\n\n const document = gql`\n {\n getAsyncIdentifiers {\n before\n after\n }\n }\n `;\n\n const firstResult$ = testkit.execute(app, {\n contextValue: {\n identifier: \"first\",\n },\n document,\n });\n\n await gettingBefore;\n\n const secondResult$ = testkit.execute(app, {\n contextValue: {\n identifier: \"second\",\n },\n document,\n });\n\n getAfter();\n\n await expect(firstResult$).resolves.toEqual({\n data: {\n getAsyncIdentifiers: {\n before: \"first\",\n after: \"first\",\n },\n },\n });\n\n await expect(secondResult$).resolves.toEqual({\n data: {\n getAsyncIdentifiers: {\n before: \"second\",\n after: \"second\",\n },\n },\n });\n});\n\nfunction createDeferred() {\n let resolve, reject;\n const promise = new Promise((res, rej) =\u003e {\n resolve = res;\n reject = rej;\n });\n return {\n promise,\n resolve,\n reject,\n };\n}\n```\n\nand execute using:\n\n```\nnpm test\n```\n\nYour project tree should look like this:\n\n```\nGHSA-53wg-r69p-v3r7\n package.json\n package-lock.json\n babel.config.json\n GHSA-53wg-r69p-v3r7.spec.js\n```\n\n#### Expected vs. Actual Outcome\n\n```diff\n- Expected - 1\n+ Received + 1\n\n Object {\n \"data\": Object {\n \"getAsyncIdentifiers\": Object {\n- \"after\": \"first\",\n+ \"after\": \"second\",\n \"before\": \"first\",\n },\n },\n }\n```\n\n### Impact\n\nAny application that uses services that inject the context using `@ExecutionContext()` from a singleton provider are at risk. The more traffic an application has, the higher the chance for parallel requests, the higher the risk.",
"id": "GHSA-53wg-r69p-v3r7",
"modified": "2026-01-21T16:20:01Z",
"published": "2026-01-16T21:09:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/graphql-hive/graphql-modules/security/advisories/GHSA-53wg-r69p-v3r7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-23735"
},
{
"type": "WEB",
"url": "https://github.com/graphql-hive/graphql-modules/issues/2613"
},
{
"type": "WEB",
"url": "https://github.com/graphql-hive/graphql-modules/pull/2521"
},
{
"type": "PACKAGE",
"url": "https://github.com/graphql-hive/graphql-modules"
},
{
"type": "WEB",
"url": "https://github.com/graphql-hive/graphql-modules/releases/tag/release-1768575025568"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "GraphQL Modules has a Race Condition issue"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.