VYPR
Medium severity6.7GHSA Advisory· Published Jun 12, 2026

LangGraph has NoSQL parameter injection in MongoDBSaver, allowing cross-tenant state access

CVE-2026-48121

Description

NoSQL injection in MongoDBSaver allows attackers to inject MongoDB operators via configurable checkpoint identifiers, leading to cross-tenant checkpoint disclosure.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

NoSQL injection in MongoDBSaver allows attackers to inject MongoDB operators via configurable checkpoint identifiers, leading to cross-tenant checkpoint disclosure.

Vulnerability

In MongoDBSaver.getTuple() and related methods, the fields thread_id, checkpoint_ns, and checkpoint_id from config.configurable are passed directly into MongoDB find() queries without type validation [1][2]. This allows an attacker to supply object payloads containing MongoDB operators (e.g., { "$gt": "" }, { "$ne": null }) instead of plain strings. The vulnerability affects all versions of @langchain/langgraph-checkpoint-mongodb prior to 1.3.1 [1][4]. Applications are exposed when untrusted input is forwarded into config.configurable without string coercion or schema validation [2].

Exploitation

An attacker needs the ability to control the configurable object passed to app.invoke(), app.stream(), or direct saver methods. This is typically achieved by sending a JSON request body where thread_id, checkpoint_ns, or checkpoint_id are objects containing MongoDB operators [2]. No authentication is required if the application blindly forwards user-supplied values. The attacker can then call getTuple() or list() with the malicious config, causing MongoDB to interpret the operators and return checkpoints from other threads or tenants [2][4].

Impact

Successful exploitation results in unauthorized read access to checkpoint data, including checkpoint state, metadata, and pending writes [4]. This is a confidentiality issue with cross-tenant data disclosure risk, as an attacker can retrieve checkpoints belonging to other users or threads [2][4]. The attacker does not gain write access, but the information leakage can compromise sensitive application state.

Mitigation

The vulnerability is fixed in @langchain/langgraph-checkpoint-mongodb@1.3.1, released on 2026-06-12 via pull request #2397 [1]. Users should upgrade to this version immediately. As a workaround, applications can enforce strict string validation on all configurable checkpoint identifiers before passing them to LangGraph methods, or use only server-issued identifiers [2][4]. No other mitigations are available.

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
284226c7ca16

fix(langgraph-checkpoint-mongodb): validate configurable checkpoint IDs (#2397)

https://github.com/langchain-ai/langgraphjsHunter LovellMay 18, 2026via body-scan-shorthand
3 files changed · +293 35
  • .changeset/khaki-pugs-cheer.md+10 0 added
    @@ -0,0 +1,10 @@
    +---
    +"@langchain/langgraph-checkpoint-mongodb": patch
    +---
    +
    +fix(checkpoint-mongodb): validate configurable checkpoint identifiers before queries
    +
    +Add runtime validation for `thread_id`, `checkpoint_ns`, and `checkpoint_id` in
    +`MongoDBSaver` methods that read and write checkpoints. This prevents object-based
    +operator payloads from being passed into MongoDB query filters and ensures invalid
    +configurable values fail fast with explicit errors.
    
  • libs/checkpoint-mongodb/src/checkpoint.ts+89 35 modified
    @@ -23,6 +23,25 @@ export type MongoDBSaverParams = {
       enableTimestamps?: boolean;
     };
     
    +function getStringConfigValue(
    +  name: string,
    +  value: unknown,
    +  { required = false }: { required?: boolean } = {}
    +): string | undefined {
    +  if (value === undefined) {
    +    if (required) {
    +      throw new Error(`Invalid configurable.${name}: expected a string`);
    +    }
    +    return undefined;
    +  }
    +
    +  if (value === null || typeof value !== "string") {
    +    throw new Error(`Invalid configurable.${name}: expected a string`);
    +  }
    +
    +  return value;
    +}
    +
     /**
      * A LangGraph checkpoint saver backed by a MongoDB database.
      */
    @@ -73,11 +92,23 @@ export class MongoDBSaver extends BaseCheckpointSaver {
        * for the given thread ID is retrieved.
        */
       async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
    -    const {
    -      thread_id,
    -      checkpoint_ns = "",
    -      checkpoint_id,
    -    } = config.configurable ?? {};
    +    const thread_id = getStringConfigValue(
    +      "thread_id",
    +      config.configurable?.thread_id
    +    );
    +    if (thread_id === undefined) {
    +      return undefined;
    +    }
    +    const checkpoint_ns =
    +      getStringConfigValue(
    +        "checkpoint_ns",
    +        config.configurable?.checkpoint_ns
    +      ) ?? "";
    +    const checkpoint_id = getStringConfigValue(
    +      "checkpoint_id",
    +      config.configurable?.checkpoint_id
    +    );
    +
         let query;
         if (checkpoint_id) {
           query = {
    @@ -155,16 +186,20 @@ export class MongoDBSaver extends BaseCheckpointSaver {
       ): AsyncGenerator<CheckpointTuple> {
         const { limit, before, filter } = options ?? {};
         const query: Record<string, unknown> = {};
    +    const thread_id = getStringConfigValue(
    +      "thread_id",
    +      config.configurable?.thread_id
    +    );
    +    const checkpoint_ns = getStringConfigValue(
    +      "checkpoint_ns",
    +      config.configurable?.checkpoint_ns
    +    );
     
    -    if (config?.configurable?.thread_id) {
    -      query.thread_id = config.configurable.thread_id;
    +    if (thread_id) {
    +      query.thread_id = thread_id;
         }
    -
    -    if (
    -      config?.configurable?.checkpoint_ns !== undefined &&
    -      config?.configurable?.checkpoint_ns !== null
    -    ) {
    -      query.checkpoint_ns = config.configurable.checkpoint_ns;
    +    if (checkpoint_ns !== undefined) {
    +      query.checkpoint_ns = checkpoint_ns;
         }
     
         if (filter) {
    @@ -179,8 +214,12 @@ export class MongoDBSaver extends BaseCheckpointSaver {
           });
         }
     
    -    if (before) {
    -      query.checkpoint_id = { $lt: before.configurable?.checkpoint_id };
    +    if (before?.configurable?.checkpoint_id !== undefined) {
    +      const before_checkpoint_id = getStringConfigValue(
    +        "checkpoint_id",
    +        before.configurable?.checkpoint_id
    +      );
    +      query.checkpoint_id = { $lt: before_checkpoint_id };
         }
     
         let result = this.db
    @@ -234,14 +273,22 @@ export class MongoDBSaver extends BaseCheckpointSaver {
         checkpoint: Checkpoint,
         metadata: CheckpointMetadata
       ): Promise<RunnableConfig> {
    -    const thread_id = config.configurable?.thread_id;
    -    const checkpoint_ns = config.configurable?.checkpoint_ns ?? "";
    +    const thread_id = getStringConfigValue(
    +      "thread_id",
    +      config.configurable?.thread_id,
    +      { required: true }
    +    );
    +    const checkpoint_ns =
    +      getStringConfigValue(
    +        "checkpoint_ns",
    +        config.configurable?.checkpoint_ns
    +      ) ?? "";
    +    const parent_checkpoint_id = getStringConfigValue(
    +      "checkpoint_id",
    +      config.configurable?.checkpoint_id
    +    );
         const checkpoint_id = checkpoint.id;
    -    if (thread_id === undefined) {
    -      throw new Error(
    -        `The provided config must contain a configurable field with a "thread_id" field.`
    -      );
    -    }
    +
         const [
           [checkpointType, serializedCheckpoint],
           [metadataType, serializedMetadata],
    @@ -254,7 +301,7 @@ export class MongoDBSaver extends BaseCheckpointSaver {
           throw new Error("Mismatched checkpoint and metadata types.");
         }
         const doc = {
    -      parent_checkpoint_id: config.configurable?.checkpoint_id,
    +      parent_checkpoint_id,
           type: checkpointType,
           checkpoint: serializedCheckpoint,
           metadata: serializedMetadata,
    @@ -289,18 +336,21 @@ export class MongoDBSaver extends BaseCheckpointSaver {
         writes: PendingWrite[],
         taskId: string
       ): Promise<void> {
    -    const thread_id = config.configurable?.thread_id;
    -    const checkpoint_ns = config.configurable?.checkpoint_ns;
    -    const checkpoint_id = config.configurable?.checkpoint_id;
    -    if (
    -      thread_id === undefined ||
    -      checkpoint_ns === undefined ||
    -      checkpoint_id === undefined
    -    ) {
    -      throw new Error(
    -        `The provided config must contain a configurable field with "thread_id", "checkpoint_ns" and "checkpoint_id" fields.`
    -      );
    -    }
    +    const thread_id = getStringConfigValue(
    +      "thread_id",
    +      config.configurable?.thread_id,
    +      { required: true }
    +    );
    +    const checkpoint_ns = getStringConfigValue(
    +      "checkpoint_ns",
    +      config.configurable?.checkpoint_ns,
    +      { required: true }
    +    );
    +    const checkpoint_id = getStringConfigValue(
    +      "checkpoint_id",
    +      config.configurable?.checkpoint_id,
    +      { required: true }
    +    );
     
         const operations = await Promise.all(
           writes.map(async ([channel, value], idx) => {
    @@ -333,6 +383,10 @@ export class MongoDBSaver extends BaseCheckpointSaver {
       }
     
       async deleteThread(threadId: string) {
    +    if (typeof threadId !== "string") {
    +      throw new Error("Invalid threadId: expected a string");
    +    }
    +
         await this.db
           .collection(this.checkpointCollectionName)
           .deleteMany({ thread_id: threadId });
    
  • libs/checkpoint-mongodb/src/tests/checkpoints.test.ts+194 0 modified
    @@ -121,4 +121,198 @@ describe("MongoDBSaver", () => {
           expect(result.done).toBe(true);
         });
       });
    +
    +  describe("configurable validation", () => {
    +    it("should return undefined when thread_id is missing in getTuple", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(saver.getTuple({ configurable: {} })).resolves.toBeUndefined();
    +    });
    +
    +    it("should reject object thread_id in getTuple", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.getTuple({
    +          configurable: { thread_id: { $gt: "" }, checkpoint_ns: "" },
    +        } as never)
    +      ).rejects.toThrow('Invalid configurable.thread_id: expected a string');
    +    });
    +
    +    it("should reject object checkpoint_ns in getTuple", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.getTuple({
    +          configurable: { thread_id: "safe-thread", checkpoint_ns: { $ne: null } },
    +        } as never)
    +      ).rejects.toThrow('Invalid configurable.checkpoint_ns: expected a string');
    +    });
    +
    +    it("should reject object checkpoint_id in getTuple", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.getTuple({
    +          configurable: {
    +            thread_id: "safe-thread",
    +            checkpoint_ns: "",
    +            checkpoint_id: { $gt: "" },
    +          },
    +        } as never)
    +      ).rejects.toThrow('Invalid configurable.checkpoint_id: expected a string');
    +    });
    +
    +    it("should reject object thread_id in list", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      const generator = saver.list({
    +        configurable: { thread_id: { $gt: "" } },
    +      } as never);
    +      await expect(generator.next()).rejects.toThrow(
    +        'Invalid configurable.thread_id: expected a string'
    +      );
    +    });
    +
    +    it("should reject object checkpoint_ns in list", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      const generator = saver.list({
    +        configurable: { thread_id: "safe-thread", checkpoint_ns: { $ne: null } },
    +      } as never);
    +      await expect(generator.next()).rejects.toThrow(
    +        'Invalid configurable.checkpoint_ns: expected a string'
    +      );
    +    });
    +
    +    it("should reject object before.checkpoint_id in list", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      const generator = saver.list(
    +        { configurable: { thread_id: "safe-thread", checkpoint_ns: "" } },
    +        { before: { configurable: { checkpoint_id: { $lt: "zzz" } } } as never }
    +      );
    +      await expect(generator.next()).rejects.toThrow(
    +        'Invalid configurable.checkpoint_id: expected a string'
    +      );
    +    });
    +
    +    it("should reject non-string thread_id in put", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +      const checkpoint = {
    +        v: 4,
    +        id: "cp-1",
    +        ts: new Date().toISOString(),
    +        channel_values: {},
    +        channel_versions: {},
    +        versions_seen: {},
    +      } as never;
    +
    +      await expect(
    +        saver.put(
    +          { configurable: { thread_id: { $gt: "" } } } as never,
    +          checkpoint,
    +          { source: "input", step: 1, parents: {} } as never
    +        )
    +      ).rejects.toThrow('Invalid configurable.thread_id: expected a string');
    +    });
    +
    +    it("should reject non-string thread_id in putWrites", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.putWrites(
    +          {
    +            configurable: {
    +              thread_id: { $gt: "" },
    +              checkpoint_ns: "",
    +              checkpoint_id: "cp-1",
    +            },
    +          } as never,
    +          [["foo", "bar"]],
    +          "task-1"
    +        )
    +      ).rejects.toThrow('Invalid configurable.thread_id: expected a string');
    +    });
    +
    +    it("should reject non-string checkpoint_ns in putWrites", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.putWrites(
    +          {
    +            configurable: {
    +              thread_id: "safe-thread",
    +              checkpoint_ns: { $ne: null },
    +              checkpoint_id: "cp-1",
    +            },
    +          } as never,
    +          [["foo", "bar"]],
    +          "task-1"
    +        )
    +      ).rejects.toThrow('Invalid configurable.checkpoint_ns: expected a string');
    +    });
    +
    +    it("should reject non-string checkpoint_id in putWrites", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(
    +        saver.putWrites(
    +          {
    +            configurable: {
    +              thread_id: "safe-thread",
    +              checkpoint_ns: "",
    +              checkpoint_id: { $gt: "" },
    +            },
    +          } as never,
    +          [["foo", "bar"]],
    +          "task-1"
    +        )
    +      ).rejects.toThrow('Invalid configurable.checkpoint_id: expected a string');
    +    });
    +
    +    it("should reject non-string threadId in deleteThread", async () => {
    +      const client = createMockClient();
    +      const saver = new MongoDBSaver({
    +        client: client as unknown as MongoClient,
    +      });
    +
    +      await expect(saver.deleteThread({} as never)).rejects.toThrow(
    +        "Invalid threadId: expected a string"
    +      );
    +    });
    +  });
     });
    

Vulnerability mechanics

Root cause

"MongoDB query operators injected via unvalidated configurable checkpoint identifiers allow cross-tenant data access."

Attack vector

An attacker who can control values in `config.configurable` (e.g. by forwarding a JSON request body's `thread_id` / `checkpoint_ns` / `checkpoint_id` without string coercion) can supply MongoDB operator objects such as `{"$gt": ""}` or `{"$ne": null}`. Because the vulnerable code passes these values directly into `find()` queries, the operators are interpreted as query conditions rather than literal identifiers, matching checkpoints outside the attacker's intended thread boundary [ref_id=1]. This results in cross-tenant disclosure of checkpoint state, metadata, and pending writes [CWE-943].

Affected code

The vulnerable path is in `MongoDBSaver.getTuple()` inside `libs/checkpoint-mongodb/src/checkpoint.ts`, where `thread_id`, `checkpoint_ns`, and `checkpoint_id` from `config.configurable` were forwarded directly into MongoDB `find()` queries without type validation. The same unvalidated values were also reused in `put()`, `putWrites()`, and `list()`, and a separate `find()` lookup on `checkpoint_writes` also lacked guards. Notably, the `list()` method already had a filter-object guard (as noted in the advisory), making the inconsistent coverage the root cause.

What the fix does

The patch adds a shared `getStringConfigValue()` helper in `libs/checkpoint-mongodb/src/checkpoint.ts` that rejects any value that is not a string (including objects, arrays, and null) with an explicit error message [patch_id=5722061]. This validator is applied to `getTuple`, `list`, `put`, `putWrites`, and `deleteThread` so that operator-like payloads are caught before they reach any MongoDB query or write path. The patch also preserves the existing behavior where `getTuple` returns `undefined` when `thread_id` is missing, while now correctly rejecting non-string values.

Preconditions

  • configApplication must use `@langchain/langgraph-checkpoint-mongodb` with a multi-tenant or user-isolated thread model.
  • inputApplication must accept user-controlled values for `thread_id`, `checkpoint_ns`, or `checkpoint_id` (e.g. from JSON request bodies or query parameters) and forward them into `config.configurable` without type validation.

Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.