# @envelop/graphql-modules has a Race Condition vulnerability

useGraphQLModules in @envelop/graphql-modules bypasses graphql-modules' async_hooks context isolation, so parallel requests bleed ExecutionContext between services.

- Payout: Research
- Severity: high
- Program: @envelop/graphql-modules
- Found by: DuckThom
- Disclosed: 2026-01-21
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h3hw-29fv-2x75
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-h3hw-29fv-2x75
- Tags: area:web3 domain:api impact:data-leak kind:research platform:ghsa sev:high tech:graphql tech:nodejs technique:incomplete-fix technique:race-window vuln:info-disclosure vuln:race-condition

**Target:** GraphQL Yoga plugin wrapping graphql-modules application
**Root cause:** useGraphQLModules re-implements the graphql-modules execution path without the async_hooks-based context tracking added in graphql-modules 2.4.1/3.1.1, so the singleton injector's ExecutionContext is shared across concurrent requests.
**Steps:**
1. Install @envelop/graphql-modules ^9.0.0, graphql-yoga ^5, graphql-modules 3.1.1, reflect-metadata.
2. Create a module with a singleton @Injectable() Service using `@ExecutionContext() private context` and a `get()` returning `this.context.requestId`.
3. Wire Yoga with `plugins: [useGraphQLModules(application)]` and a context() returning `{ requestId: randomUUID() }`.
4. Resolver `id1` returns `injector.get(Service).get()` immediately; `id2` awaits 100ms then returns the same.
5. Fire 2 parallel `query { books { id1 id2 } }` requests via `Promise.all`; observe id1 !== id2, proving context bleed.
**How found:** Noticed the graphql-modules async_hooks fix was bypassed when the app is mounted through the Envelop plugin rather than graphql-modules' own Yoga integration; built a two-field resolver with a delay to expose the interleaving.
**Insight:** When a framework patches a concurrency bug via async_hooks, any wrapper/plugin that re-implements the request lifecycle can silently drop that isolation—test context-scoped singletons under parallel load, not just single requests.

## Proof of concept (copied exactly from the original report)

**Bleed test asserting id1 equals id2**

```
import axios from 'axios';

const url = 'http://localhost:4001/graphql';
const query = `query { books { id1 id2 } }`;

const makeGraphQLRequest = async () => {
    const response = await axios.post(url, { query });

    const book = response.data.data.books[0]
    if (book.id1 !== book.id2) {
        throw new Error(`wrong response with ids ${(book.id1)} and ${(book.id2)}`)
    }
}

const numberOfRequests = 2;
await Promise.all(Array.from(
    { length: numberOfRequests },
    makeGraphQLRequest,
));
```

**Service injecting ExecutionContext via decorator**

```
import { ExecutionContext, Injectable } from 'graphql-modules';
import 'reflect-metadata';

@Injectable()
export default class Service {
    @ExecutionContext()
    private context: ExecutionContext;

    get() {
        return this.context.requestId;
    }
}
```

**Yoga setup with useGraphQLModules plugin**

```
const application = createApplication({
    modules: [module]
})

const yoga = createYoga({
    schema: application.schema,
    plugins: [useGraphQLModules(application)],
    context() {
        return {
            requestId: randomUUID(),
        }
    }
})

const server = createServer(yoga)
```

