Security
Headlines
HeadlinesLatestCVEs

Headline

GHSA-53wg-r69p-v3r7: GraphQL Modules has a Race Condition issue

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() are at risk. The more traffic an application has, the higher the chance for parallel requests, the higher the risk.

ghsa
#nodejs#js#git#auth

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() are at risk. The more traffic an application has, the higher the chance for parallel requests, the higher the risk.

References

  • GHSA-53wg-r69p-v3r7
  • graphql-hive/graphql-modules#2613
  • graphql-hive/graphql-modules#2521
  • https://github.com/graphql-hive/graphql-modules/releases/tag/release-1768575025568
  • https://nvd.nist.gov/vuln/detail/CVE-2026-23735

ghsa: Latest News

GHSA-8qq5-rm4j-mr97: node-tar is Vulnerable to Arbitrary File Overwrite and Symlink Poisoning via Insufficient Path Sanitization