LangGraph has NoSQL parameter injection in MongoDBSaver, allowing cross-tenant state access
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- Range: <= 1.3.0
Patches
1284226c7ca16fix(langgraph-checkpoint-mongodb): validate configurable checkpoint IDs (#2397)
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- github.com/advisories/GHSA-98xf-r82g-9mhxghsaADVISORY
- github.com/langchain-ai/langgraphjs/commit/284226c7ca164b3c81fe2d9e32b10f1fc6b99a3cghsa
- github.com/langchain-ai/langgraphjs/issues/2351ghsa
- github.com/langchain-ai/langgraphjs/pull/2397ghsa
- github.com/langchain-ai/langgraphjs/security/advisories/GHSA-98xf-r82g-9mhxghsa
News mentions
0No linked articles in our index yet.