GHSA-53WG-R69P-V3R7

Vulnerability from github – Published: 2026-01-16 21:09 – Updated: 2026-01-21 16:20
VLAI?
Summary
GraphQL Modules has a Race Condition issue
Details

Summary

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.

Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…