+ "details": "### Summary\nContext race condition when using `useGraphQLModules` plugin\n\n### Details\n\nRelated to: https://github.com/graphql-hive/graphql-modules/security/advisories/GHSA-53wg-r69p-v3r7\n\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() and graphql-modules are used in Yoga with `useGraphQLModules(application)`. This issue was fixed in `graphql-modules` in `2.4.1` and `3.1.1` but using `useGraphQLModules` will bypass the `async_hooks` fix that was implemented.\n\n### PoC\n\nCreate the following `package.json` and run `npm i`\n\n```json\n{\n \"name\": \"poc\",\n \"scripts\": {\n \"compile\": \"tsc\",\n \"start\": \"npm run compile && node ./dist/src/index.js\",\n \"test\": \"npm run compile && node ./dist/test/bleedtest.js\"\n },\n \"dependencies\": {\n \"@envelop/graphql-modules\": \"^9.0.0\",\n \"graphql-yoga\": \"^5.0.0\",\n \"graphql\": \"^16.10.0\",\n \"graphql-modules\": \"3.1.1\",\n \"reflect-metadata\": \"0.2.1\",\n \"axios\": \"^1.8.4\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^22.14.1\",\n \"typescript\": \"^5.8.3\"\n }\n}\n```\n\nDefine the app entrypoint: `src/index.ts`\n\n```ts\nimport { module } from \"./module.js\";\nimport { useGraphQLModules } from '@envelop/graphql-modules'\nimport { createApplication } from \"graphql-modules\";\nimport { createServer } from 'node:http'\nimport { randomUUID } from \"node:crypto\";\nimport { createYoga } from 'graphql-yoga';\n\nconst application = createApplication({\n modules: [module]\n})\n\nconst yoga = createYoga({\n schema: application.schema,\n plugins: [useGraphQLModules(application)],\n context() {\n return {\n requestId: randomUUID(),\n }\n }\n})\n\nconst server = createServer(yoga)\nserver.listen(4001, '127.0.0.1', undefined, () => {\n console.info(\n `[Server] Running on http://localhost:4001/graphql`\n )\n})\n```\n\nCreate the test module: `src/module.ts`\n\n```ts\nimport { createModule, gql } from \"graphql-modules\";\nimport Service from \"./service.js\";\n\nconst typeDefs = gql`\n type Book {\n id1: String\n id2: String\n }\n type Query {\n books: [Book]\n }\n`;\n\nexport const module = createModule({\n id: 'book-module',\n typeDefs: [typeDefs],\n providers: [Service],\n resolvers: {\n Query: {\n // return one empty book\n books: () => [{}],\n },\n Book: {\n // return the requestId from the context\n id1: async (_root, _args, { injector } ) => {\n return injector.get(Service).get();\n },\n // return the requestId from the context of the service 100 ms later\n id2: async (_root, _args, { injector } ) => {\n await new Promise(resolve => setTimeout(resolve, 100));\n return injector.get(Service).get()\n },\n }\n }\n})\n```\n\nAdd the Service that's to be injected `src/service.ts`\n\n```ts\nimport { ExecutionContext, Injectable } from 'graphql-modules';\nimport 'reflect-metadata';\n\n@Injectable()\nexport default class Service {\n @ExecutionContext()\n private context: ExecutionContext;\n\n get() {\n return this.context.requestId;\n }\n}\n```\n\nAdd the test case `test/bleedtest.js`\n\n```ts\nimport axios from 'axios';\n\nconst url = 'http://localhost:4001/graphql';\nconst query = `query { books { id1 id2 } }`;\n\nconst makeGraphQLRequest = async () => {\n const response = await axios.post(url, { query });\n\n const book = response.data.data.books[0]\n if (book.id1 !== book.id2) {\n throw new Error(`wrong response with ids ${(book.id1)} and ${(book.id2)}`)\n }\n}\n\nconst numberOfRequests = 2;\nawait Promise.all(Array.from(\n { length: numberOfRequests },\n makeGraphQLRequest,\n));\n```\n\nThen run the server with `npm run start` in one terminal and the testcase in another with `npm run test`.\nThe returned IDs should be identical as they are both read from the context within the same request.\nHowever, there is a mismatch:\n\n```\n❯ npm run test\n\n> poc@1.0.0 test\n> npm run compile && node ./dist/test/bleedtest.js\n\n\n> poc@1.0.0 compile\n> tsc\n\nfile://<redacted>/dist/test/bleedtest.js:8\n throw new Error(`wrong response with ids ${(book.id1)} and ${(book.id2)}`);\n ^\n\nError: wrong response with ids c2d83151-0922-4f25-a3e9-2f03acc1376a and e16c7335-0eaa-4386-b415-869ee4b05315\n at makeGraphQLRequest (file://<redacted>/dist/test/bleedtest.js:8:15)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async Promise.all (index 0)\n at async file://<redacted>/dist/test/bleedtest.js:12:1\n```\n\n### Impact\nAny application that uses `useGraphQLModules` from `@envelop/graphql-modules` along with 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.",
0 commit comments