Oct 24, 2026·9 min read·3 visits
LangGraph relational stores (PostgreSQL and SQLite) flatten namespace tuples into dot-separated strings (e.g., ('foo', 'bar') becomes 'foo.bar'). Prior to the patch, only write operations validated against periods in namespace labels. Read, search, delete, and direct batch operations omitted this check, allowing a caller supplying ('foo.bar',) to collide with ('foo', 'bar') and bypass multi-tenant namespace isolation.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
LangGraph provides a persistent storage abstraction (BaseStore) used to manage long-term memory, workflow state, and agent checkpoints across sessions. Within this architecture, data is organized hierarchically using namespaces represented in Python as tuples of string labels, such as ("tenant_id", "user_id", "memories"). In multi-tenant deployments, applications rely on the strict separation of these namespace tuples to isolate state and confidential data between distinct users or organizations.
In the relational database backends—specifically PostgresStore, AsyncPostgresStore, SqliteStore, and AsyncSqliteStore—the storage layer maps Python namespace tuples into flat SQL column representations. To achieve this, the backends serialize the tuple into a single string by joining the individual elements with a period (.) delimiter via ".".join(namespace). Under this serialization scheme, the boundary between namespace hierarchy levels is represented exclusively by the period character in the underlying database table.
CVE-2026-115883 (GHSA-6hmw-9432-67qq) arises from asymmetric and incomplete input validation across the BaseStore interface and its concrete database implementations. While standard write operations (put and aput) enforced validation prohibiting periods within individual namespace labels, read (get), query (search, list_namespaces), deletion (delete), and batch (batch, abatch) operations failed to validate label syntax.
Consequently, if an application constructs a namespace tuple using unvalidated input on a read, search, or delete path, an attacker can supply a label containing a period to forge a composite namespace path. When the relational backend flattens the tuple for SQL execution, a single-element tuple ("tenant_a.secret",) resolves to the exact same database identifier as the two-element tuple ("tenant_a", "secret"). This collision breaks namespace isolation (CWE-20, CWE-863), enabling unauthorized cross-namespace data retrieval and deletion.
The root cause of CVE-2026-115883 is a delimiter collision resulting from inconsistent enforcement of domain invariants across different API entry points in the langgraph-checkpoint storage hierarchy. Whenever a structured data type (such as a tuple of strings) is serialized into a flat string using an unescaped delimiter, the serialization is only injective (one-to-one) if the delimiter character is strictly forbidden within the constituent elements.
In versions prior to the fix, the LangGraph BaseStore class recognized this invariant and implemented validation to reject empty labels, non-string labels, and labels containing the . character. However, the developers applied this check exclusively inside the high-level put and aput methods. The remaining high-level methods on BaseStore and AsyncBatchedBaseStore—specifically get, aget, search, asearch, delete, adelete, list_namespaces, and alist_namespaces—constructed operation objects (GetOp, SearchOp, PutOp, ListNamespacesOp) and dispatched them to the backend without invoking the label validator.
Furthermore, a secondary architectural bypass existed in the batch execution pipeline. The BaseStore interface exposes batch(ops) and abatch(ops) methods, which accept an iterable of raw operation objects (GetOp, PutOp, SearchOp, ListNamespacesOp) directly from the caller. Because callers could instantiate PutOp(namespace=("foo.bar",), key="k", value={...}) or GetOp(namespace=("foo.bar",), key="k") and pass them directly into store.batch([...]), even the validation present on store.put() could be completely circumvented.
Note that InMemoryStore is unaffected by this vulnerability. Rather than serializing namespace tuples into dot-delimited strings for SQL storage, InMemoryStore keys its internal Python dictionaries using the raw tuple objects directly. In Python, ("foo.bar",) != ("foo", "bar"), preserving isolation in memory-backed stores despite the missing validation on read and batch paths.
Commit bfcfea554ed5c7f7be562cebf8825e911b493ab1 addresses the vulnerability by centralizing namespace validation and enforcing it across both high-level convenience methods and low-level batch dispatchers. In libs/checkpoint/langgraph/store/base/__init__.py, the patch formalizes _validate_namespace_labels and introduces a comprehensive operation validator, validate_op_namespace, which inspects all four operation types.
# libs/checkpoint/langgraph/store/base/__init__.py
def _validate_namespace_labels(namespace: tuple[str, ...]) -> None:
for label in namespace:
if not isinstance(label, str):
raise InvalidNamespaceError(
f"Namespace labels must be strings. Got {label} of type {type(label)} in {namespace}"
)
if "." in label:
raise InvalidNamespaceError(
f"Namespace labels cannot contain periods ('.'). Got {label} in {namespace}"
)
if not label:
raise InvalidNamespaceError(
f"Namespace labels cannot be empty strings. Got {label} in {namespace}"
)
def validate_op_namespace(op: Op) -> None:
# Enforce label validation on direct operation structures
if isinstance(op, (GetOp, PutOp)):
_validate_namespace_labels(op.namespace)
elif isinstance(op, SearchOp):
_validate_namespace_labels(op.namespace_prefix)
elif isinstance(op, ListNamespacesOp):
for condition in op.match_conditions or ():
_validate_namespace_labels(condition.path)In both libs/checkpoint/langgraph/store/base/__init__.py (BaseStore) and libs/checkpoint/langgraph/store/base/batch.py (AsyncBatchedBaseStore), the patch injects explicit calls to _validate_namespace_labels into every method that accepts a namespace or match condition. Notably, in BaseStore.delete and AsyncBatchedBaseStore.adelete (which internally emit a PutOp with value=None), adding validation prevents unauthorized deletion of records in adjacent namespaces.
# libs/checkpoint/langgraph/store/base/__init__.py
def get(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: bool | None = None,
) -> Item | None:
+ _validate_namespace_labels(namespace)
return self.batch(
[GetOp(namespace, str(key), refresh_ttl=_ensure_refresh(refresh_ttl))]
)[0]
def delete(self, namespace: tuple[str, ...], key: str) -> None:
+ _validate_namespace_labels(namespace)
self.batch([PutOp(namespace, str(key), None, ttl=None)])To close the direct batch() and abatch() bypass vector in the database backends, the patch modifies _group_ops inside libs/checkpoint-postgres/langgraph/store/postgres/base.py and libs/checkpoint-sqlite/langgraph/store/sqlite/base.py. Before grouping operations by type for SQL execution, _group_ops now iterates through every incoming Op and calls validate_op_namespace(op). Similarly, AsyncBatchedBaseStore calls validate_op_namespace(op) inside abatch prior to enqueuing operations into the async processing pipeline.
# libs/checkpoint-postgres/langgraph/store/postgres/base.py
# and libs/checkpoint-sqlite/langgraph/store/sqlite/base.py
def _group_ops(ops: Iterable[Op]) -> tuple[dict[type, list[tuple[int, Op]]], int]:
grouped_ops: dict[type, list[tuple[int, Op]]] = defaultdict(list)
tot = 0
for idx, op in enumerate(ops):
+ validate_op_namespace(op)
grouped_ops[type(op)].append((idx, op))
tot += 1
return grouped_ops, totThe fix is complete for the affected storage backends. By enforcing validation both at the API surface (get, search, delete, put, list_namespaces) and at the low-level batch ingestion point (_group_ops and AsyncBatchedBaseStore.abatch), no operation containing a period in a namespace label can reach the SQL query generation logic, regardless of whether it was invoked synchronously, asynchronously, or via raw Op batching.
Exploiting CVE-2026-115883 requires a target deployment that uses either PostgreSQL (langgraph-checkpoint-postgres) or SQLite (langgraph-checkpoint-sqlite) as its persistent store and incorporates user-controlled input into at least one element of a namespace tuple. This pattern commonly occurs in multi-tenant AI agent platforms where namespaces are structured such as (tenant_id, user_id, thread_id) or (user_provided_category,).
Consider a scenario where an application stores data for different organizational divisions under ("org", department_name) or scopes queries using a prefix (user_supplied_prefix, "config"). If a legitimate tenant stores sensitive configuration or conversation history under the namespace ("foo", "bar") with key "api_credentials", the database backend serializes the namespace column to the string "foo.bar".
An attacker with low-privileged access to an endpoint that queries or deletes items from a single-element namespace (user_input,)—or a prefix namespace where the attacker controls the leading label—supplies the string "foo.bar" as their label. The application constructs the tuple ("foo.bar",) and passes it to store.get(("foo.bar",), "api_credentials") or store.search(("foo.bar",)). Because the unpatched get and search methods do not inspect the tuple for periods, the backend flattens ("foo.bar",) into "foo.bar", matching the victim's ("foo", "bar") database records.
# Proof of Concept demonstrating cross-namespace collision in unpatched stores
from langgraph.store.base import GetOp, PutOp, SearchOp
# 1. Victim data is written to a hierarchical 2-tuple namespace ('foo', 'bar')
store.put(("foo", "bar"), "secret_key", {"token": "sensitive_value"})
# 2. Attacker supplies a single label containing a dot: ('foo.bar',)
# In unpatched versions, the following calls succeed and return/modify ('foo', 'bar') data:
leaked_item = store.get(("foo.bar",), "secret_key")
search_leaks = store.search(("foo.bar",))
# 3. Attacker can also overwrite or delete across namespaces via direct batching
# bypassing BaseStore.put() validation:
store.batch([PutOp(namespace=("foo.bar",), key="secret_key", value={"token": "compromised"})])
# 4. Or delete the victim's key directly via store.delete():
store.delete(("foo.bar",), "secret_key")As shown in the regression test suite from Pull Request #9217, an attacker who can influence batch() calls or delete() calls can also achieve cross-namespace data modification or destruction. Passing store.delete(("foo.bar",), "secret_key") emits PutOp(("foo.bar",), "secret_key", None), which deletes the row belonging to ("foo", "bar") from the PostgreSQL or SQLite table.
The primary security impact of CVE-2026-115883 is unauthorized cross-namespace data access and manipulation within multi-tenant LangGraph applications. The vulnerability is rated Medium severity with a CVSS v4.0 base score of 6.0 (CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N).
Confidentiality impact on the vulnerable system is High (VC:H). In affected PostgreSQL and SQLite deployments, an authenticated user or tenant who controls a namespace label or prefix can read arbitrary items and search across namespaces belonging to other tenants, provided the attacker's controlled label position precedes the target hierarchy levels. This exposes stored agent memory, conversation histories, prompt contexts, and any application secrets persisted in the store.
Integrity impact is rated Low (VI:L). Through store.delete / store.adelete, an attacker can delete state records belonging to other namespaces. Additionally, if the application constructs raw PutOp instances and passes them to store.batch or store.abatch, the attacker can overwrite existing items or inject new items into foreign namespaces. Availability impact (VA:N) and subsequent system impacts (SC:N/SI:N/SA:N) are rated None, as the flaw does not allow arbitrary SQL injection or database server compromise.
Attack requirements are marked Present (AT:P) because exploitation depends on two architectural preconditions: the application must use a dot-flattening relational store (PostgresStore, AsyncPostgresStore, SqliteStore, or AsyncSqliteStore), and the application code must incorporate untrusted input into namespace tuples at a position where injecting a . aligns with a valid multi-segment target namespace.
To remediate CVE-2026-115883, upgrade all LangGraph and checkpoint storage packages to the patched releases. Specifically, update langgraph to version 1.2.15 or later, langgraph-checkpoint to 4.3.0 or later, langgraph-checkpoint-postgres to 3.1.3 or later, and langgraph-checkpoint-sqlite to 3.1.2 or later.
pip install --upgrade \
"langgraph>=1.2.15" \
"langgraph-checkpoint>=4.3.0" \
"langgraph-checkpoint-postgres>=3.1.3" \
"langgraph-checkpoint-sqlite>=3.1.2"> [!NOTE]
> Upgrading only langgraph-checkpoint without upgrading langgraph-checkpoint-postgres or langgraph-checkpoint-sqlite is insufficient if your application invokes store.batch() or store.abatch() directly on synchronous or asynchronous relational stores, as _group_ops validation resides within the respective database adapter packages.
For environments where immediate package upgrades cannot be deployed, implement strict input validation at the application boundary before constructing namespace tuples. Reject any user-supplied tenant ID, user ID, or category label that is empty, is not a str instance, or contains a period (.) character prior to invoking get, search, delete, list_namespaces, or batch.
Additionally, security engineers should review how multi-tenant namespaces are structured. Ensure that fixed, server-controlled tenant identifiers (derived from verified authentication tokens rather than request parameters) occupy the root position of the namespace tuple (e.g., (verified_tenant_id, user_supplied_label)), which limits the blast radius of any label manipulation to the authenticated tenant's own hierarchy.
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
langgraph langchain-ai | < 1.2.15 | 1.2.15 |
langgraph-checkpoint-postgres langchain-ai | < 3.1.3 | 3.1.3 |
langgraph-checkpoint-sqlite langchain-ai | < 3.1.2 | 3.1.2 |
langgraph-checkpoint langchain-ai | < 4.3.0 | 4.3.0 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | CVE-2026-115883 / GHSA-6hmw-9432-67qq |
| CWE Classification | CWE-20 (Improper Input Validation) / CWE-863 (Incorrect Authorization) |
| CVSS v4.0 Score | 6.0 Medium (CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N) |
| Attack Vector | Network (via unvalidated namespace label containing '.') |
| Affected Components | PostgresStore, AsyncPostgresStore, SqliteStore, AsyncSqliteStore |
| Primary Impact | Cross-namespace unauthorized read, search, and deletion of stored items |
| Exploit Status | PoC available in upstream unit tests (PR #9217) |
| CISA KEV Status | Not Listed |
The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly, leading to incorrect authorization (CWE-863) across storage namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.
A resource allocation vulnerability (CWE-770) exists in the Server-Sent Events (SSE) parser within the Node.js undici HTTP client library. In affected versions, the EventSourceStream implementation fails to restrict the count of internal Buffer chunk references and accumulated string nodes prior to event dispatch boundaries. A malicious or compromised SSE endpoint can exploit this behavior by streaming fragmented, unterminated lines or continuous sequences of short data fields without empty separator lines, bypassing configured byte-size limits and causing a fatal Out-of-Memory (OOM) crash in the client Node.js process.
CVE-2026-109869 is a high-severity denial-of-service vulnerability in undici, the default HTTP/1.1 client and Fetch implementation for Node.js. When a server responds with more than five Content-Encoding directives and prematurely closes the connection before transmitting the body, undici allocates an internal Readable stream before enforcing the content-encoding limit. Upon rejecting the fetch promise, the stream is orphaned without an error listener or abort signal, causing the premature socket closure to emit an unhandled stream error that terminates the Node.js process.