Guide · AI and GenAI
LangGraph's October 2026 Namespace Flaw Explained: Why Dot-Joined Keys Collide
18 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The LangGraph store namespace vulnerability (GHSA-6hmw-9432-67qq, CVE-2026-115883) could let one user's request read, overwrite or delete another user's saved agent memories. It affects LangGraph's SQLite and Postgres stores; LangGraph is a Python library for building AI agents.
- A namespace is the folder path for saved memories, written as a tuple of labels like ("tenant", "bob"). These stores saved it as one text string joined with dots.
- So ("tenant", "bob") and ("tenant.bob",) became the same text, "tenant.bob". The put method rejected dots, but reads, searches, deletes and raw batches did not, so one could reach the other.
- The fix: langgraph-checkpoint-sqlite 3.1.2 or langgraph-checkpoint-postgres 3.1.3, which pull in langgraph-checkpoint 4.3.0. Dotted labels now raise InvalidNamespaceError. InMemoryStore was never affected, and apps that never put user-controlled text into namespace labels were not exposed.
- Beginners can stop after the section on why put() blocked dots but reads did not. Engineers get the exact code paths, which namespaces a dotted label can reach, and a one-minute check for your install.
What goes wrong when two different namespaces become the same key?
Data meant for one place can be read, overwritten or deleted from another. That is what happened in the LangGraph store namespace vulnerability. Two namespaces that look different in code were saved under one identical string in the database.
Picture a common setup. An agent platform serves many customer workspaces from one Postgres database. Each end user's long-term memories, such as preferences and past decisions, sit under a namespace like ("tenant", "bob"). The setup is made up, but the label confusion is the one the advisory describes.
A request names a workspace, and the platform checks its own table of who owns which workspace. An unknown name counts as a new, unclaimed workspace, so the request is allowed. Now a caller asks to search the workspace called "tenant.bob". No such workspace exists, so the check passes. Before the fix, the store turned that request into the text "tenant.bob" and returned the memories saved under ("tenant", "bob").
The platform's check made sense for tuples. The trouble was that the check thought in tuples, and the store thought in strings. This guide follows that one pair, ("tenant", "bob") and ("tenant.bob",), from the bug to the fix and to the check you can run.
Which words do you need before reading this advisory?
You need six words: store, namespace, label, tuple, delimiter and tenant. Each gets a plain meaning here.
- Store: LangGraph's long-term memory, a key-value database that an agent uses to keep facts between conversations.
- Namespace: the folder path an item lives under, such as ("tenant", "bob"). Every item is found by its namespace plus a key, like a file name inside that folder.
- Label: one part of a namespace. In ("tenant", "bob") the labels are "tenant" and "bob".
- Tuple: Python's fixed list written in round brackets. ("tenant.bob",) is a tuple with one label; the trailing comma is what makes it a tuple.
- Delimiter: a character used to glue parts into one string, here the dot. Joining ("tenant", "bob") with a dot gives "tenant.bob".
- Tenant: one customer of a shared system, such as one workspace on the platform. Keeping tenants apart is called tenant isolation.
What is the LangGraph store namespace vulnerability?
The LangGraph store namespace vulnerability is a label confusion in the SQLite and Postgres stores. They saved each namespace as its labels joined with dots, so a label containing a dot could address a different namespace. The GitHub advisory GHSA-6hmw-9432-67qq, also tracked as CVE-2026-115883, describes it.
The put method, which writes items, already rejected dotted labels. The get, search, delete and list_namespaces methods did not, nor did their async versions. Operations passed straight to batch or abatch were not checked either. So a caller who could choose a label could read, overwrite or delete items in another namespace.
The advisory names three conditions. Only the SQLite and Postgres stores are affected, because InMemoryStore compares namespaces as tuples. You rely on namespaces to keep users or tenants apart. And you are exposed only if untrusted input, meaning text a user or another system controls, ends up in namespace labels.
| Package | Affected | Fixed in | Released on PyPI | Needs |
|---|---|---|---|---|
| langgraph-checkpoint-sqlite | Below 3.1.2 | 3.1.2 | 12 October 2026 | langgraph-checkpoint 4.3.0 or newer |
| langgraph-checkpoint-postgres | Below 3.1.3 | 3.1.3 (3.2.0 also fixed) | 12 October 2026 | langgraph-checkpoint 4.3.0 or newer |
| langgraph-checkpoint | Not listed on its own | 4.3.0 adds the label checks | 12 October 2026 | Ships the validate_op_namespace helper |
| InMemoryStore (inside langgraph-checkpoint) | Not affected | Not needed | Not applicable | Compares namespaces as tuples |
Why are dot-joined keys not unique?
Because joining throws away where one label ends and the next begins. The string "tenant.bob" could have come from two labels or from one label that contains a dot. Once the boundaries are gone, the database cannot tell the two apart.
You can see this with plain Python. No LangGraph is needed, and this code only joins strings. Pause and predict before you run it: which of the two comparisons will print True?
a = ("tenant", "bob") # two labels
b = ("tenant.bob",) # one label that contains a dot
print(a == b) # compare as tuples
print(".".join(a) == ".".join(b)) # compare as joined text
print(".".join(a), ".".join(b))What just happened? It printed three lines: False, then True, then tenant.bob twice. As tuples, the two namespaces are different. As joined text, they are equal. Any database row keyed by that text is shared between them.
A railway ticket shows the idea. Write coach and berth as one code with no gap, and coach S1 berth 12 becomes S112. So does coach S11 berth 2. The coach is the first label, the berth is the second, and the code is the joined text.
Where the picture breaks: the store did use a separator, the dot. The trouble was that a label could contain a dot too, which erases the boundary just like the missing gap. Also, a ticket checker can look at the chart, but a database compares bytes and never asks.
Two namespaces, one stored key
Drag to exploreFollow both paths into the join box. Two different tuples leave it as identical text, so both point at the same database row.
Diagram connections:
- ("tenant", "bob") to Join labels with a dot (Join)
- ("tenant.bob",) to Join labels with a dot (Join)
- Join labels with a dot to Text: tenant.bob
- Text: tenant.bob to One row in the store (WHERE prefix = text)
- ("tenant", "bob"): Two labels: one real user's memories
- ("tenant.bob",): One label that contains a dot
- Join labels with a dot: Label boundaries are lost here
- Text: tenant.bob: Same string from both tuples
- One row in the store: Matched by prefix column plus key
This is not a LangGraph-only lesson. Cache keys, object storage paths, log partitions and feature-flag names are often built by joining parts with a colon, slash or dot. Each one has the same question to answer: can a part contain the glue?
Why did put() block dots but get, search and delete did not?
The check lived in one method instead of at the store's front door. In langgraph-checkpoint 4.2.0, the put method calls a helper that rejects empty labels, non-string labels and labels with a dot. The read, search, delete and listing methods went straight to the store without that helper.
Deletes are the surprising one. In LangGraph, delete is written as a put operation with no value. But the public delete method builds that operation directly, so it never ran put's checks. The batch method, which takes a list of raw operations, skipped all checks as well.
Inside the SQLite and Postgres stores, every request was turned into dot-joined text before the database lookup. A get looked for rows with exactly that text. A search matched that text or anything below it. So a dotted request found the other namespace's rows.
Where the fixed version stops a dotted label
Drag to exploreLook at the two guard points. A dotted label is rejected at the public method and again inside the SQL store, so raw batch operations are covered too.
Diagram connections:
- get(('tenant.bob',), key) to BaseStore method check
- BaseStore method check to InvalidNamespaceError (Label has a dot)
- BaseStore method check to _group_ops guard (Labels clean)
- batch([GetOp(...)]) to _group_ops guard
- _group_ops guard to SQLite or Postgres query (Labels clean)
- _group_ops guard to InvalidNamespaceError (Label has a dot)
- get(('tenant.bob',), key): Caller supplies a dotted label
- BaseStore method check: New in 4.3.0 for get, search, delete, list
- batch([GetOp(...)]): Raw operations skip the method check
- _group_ops guard: Calls validate_op_namespace on every op
- InvalidNamespaceError: Raised before any SQL is sent
- SQLite or Postgres query: Only reached by clean labels
The fix in pull request 9217 adds the label check in two layers. Every public read, search, delete and listing method now validates labels first, in both the sync and async classes. A new public helper, validate_op_namespace, also runs inside _group_ops, the one internal function every SQL store operation passes through. So raw batch operations are covered too.
The stored format did not change. In the fixed versions, the internal _namespace_to_text function still returns "tenant.bob" for ("tenant", "bob"). The fix rejects ambiguous input rather than changing the encoding, so your existing data needs no migration.
Beginners can stop here. You know what the bug is, why joined keys collide, and which versions fix it. If you use these stores, upgrading the store package is the one step to take. The rest is for engineers who run these stores or design their own key formats.
Which namespaces could a dotted label reach?
Only namespaces whose labels before the dotted one are the same as yours. This holds for get, delete and search, and for raw put operations sent through batch, since the public put already rejected dots. Each of these addresses one namespace or the subtree below it. The stored text of a clean namespace is its labels joined by dots. So the clean namespace that a dotted request lands on is simply its text split at every dot.
- ("tenant.bob",) lands on ("tenant", "bob"). The dotted label is first, so any namespace in the store is in reach.
- ("acme", "alice.notes") lands on ("acme", "alice", "notes"). It stays inside the acme subtree, but it can cross into another user's space if the user id is the second label.
- ("acme", "x") with no dots lands only on itself.
This gives you a quick exposure test for your own app. Find where untrusted text enters the namespace tuple. If the labels before it come from a trusted session, such as the logged-in tenant id, the damage stays inside that tenant. If the first label is untrusted, any namespace in the store could be reached.
A guard that pins the first label to the caller's own tenant id would have limited the damage to that tenant's subtree. A guard that looks up a label in an ownership table, as in our setup, would not have, because "tenant.bob" is simply missing from the table.
How serious is CVE-2026-115883?
GitHub rates it Moderate, with a CVSS 4.0 base score of 6.0. CVSS is the standard 0 to 10 scale for how bad a vulnerability is. The vector string spells out why the score lands in the middle.
| Vector part | Value | What it means here |
|---|---|---|
| Attack vector (AV) | Network | It can be triggered through an app's normal network requests |
| Attack complexity (AC) | Low | No race or special timing is needed |
| Attack requirements (AT) | Present | The app must put untrusted input into namespace labels |
| Privileges required (PR) | Low | The caller needs ordinary access to the app |
| User interaction (UI) | None | No victim has to click anything |
| Confidentiality (VC) | High | Another namespace's items can be read |
| Integrity (VI) | Low | Rated low, though overwrites and deletes were possible |
| Availability (VA) | None | The service itself keeps running |
The advisory lists two weakness types, CWE-20 for improper input validation and CWE-863 for incorrect authorisation. In practice, the Present attack requirement is what decides your risk. A store whose labels all come from your own code, such as fixed strings and server-side ids, was not reachable this way.
Agent memory raises the stakes when you are exposed. Long-term memories often hold names, preferences and summaries of past chats. The upgrade is small and needs no data migration, so we would apply it even if you believe every label is safe.
How do you upgrade LangGraph's stores and check the fix in 2026?
Upgrade the store package you use, confirm the versions, then run a short test that should raise an error. The steps below take a few minutes.
Find what you run
Run pip show langgraph-checkpoint langgraph-checkpoint-sqlite langgraph-checkpoint-postgres. Note each Version line. Only the store package you use needs upgrading.
Upgrade the store package
For SQLite, install langgraph-checkpoint-sqlite 3.1.2 or newer. For Postgres, install langgraph-checkpoint-postgres 3.1.3 or newer. Both require langgraph-checkpoint 4.3.0 or newer, and pip installs it with them.
Update your lock file
If you use a lock file, from pip-tools, Poetry or uv, regenerate it and commit it. Otherwise your next deploy may reinstall the old versions.
Confirm the versions
Run pip show again. Expect Version: 4.3.0 or higher for langgraph-checkpoint, and 3.1.2 or 3.1.3 or higher for the store package you use. The store package version is what decides whether you are fixed.
Run the behaviour check
Run the matching script below. Each one sends a dotted label through the store package's own guard, so it should print a line starting with Fixed. NOT FIXED means your store package is still old.
Handle the new error
Reads, searches, deletes and listings with bad labels now raise InvalidNamespaceError, a subclass of ValueError. Before, the same calls returned a result instead, which was None or an empty list when nothing matched. Catch it where you accept labels from users and return a 400 error.
pip install -U "langgraph-checkpoint-sqlite>=3.1.2"
# or, for Postgres:
pip install -U "langgraph-checkpoint-postgres>=3.1.3"
pip show langgraph-checkpoint langgraph-checkpoint-sqliteOn a clean environment that had langgraph-checkpoint-sqlite 3.1.1 and langgraph-checkpoint 4.2.0, pip planned exactly two upgrades. They were langgraph-checkpoint 4.3.0 and langgraph-checkpoint-sqlite 3.1.2. If you try to pin the new store with langgraph-checkpoint 4.2.0, pip refuses with a ResolutionImpossible error.
This is the behaviour check for the SQLite store. It sends one raw batch operation with the dotted label to an empty, throwaway in-memory database. Raw batch operations skip the public method checks, so only the store package's own guard can stop this one.
from langgraph.store.sqlite import SqliteStore
from langgraph.store.base import GetOp, InvalidNamespaceError
with SqliteStore.from_conn_string(":memory:") as store:
store.setup()
try:
# a raw batch op goes through the store package's own guard
store.batch([GetOp(("tenant.bob",), "profile")])
print("NOT FIXED: the store accepted a dotted label")
except InvalidNamespaceError as err:
print("Fixed:", err)On langgraph-checkpoint 4.3.0 with langgraph-checkpoint-sqlite 3.1.2 (Python 3.13), it printed one line. That line was: Fixed: Invalid namespace label 'tenant.bob' found in ('tenant.bob',). Namespace labels cannot contain periods ('.'). On older store packages, nothing in the batch path checks labels, so the script prints NOT FIXED.
The Postgres package does not install the SQLite one, so Postgres users need a different check. This one needs no database. It calls _group_ops, an internal function that may change in later releases, so treat it as a quick check rather than an API.
from langgraph.store.base import GetOp, InvalidNamespaceError
from langgraph.store.postgres.base import _group_ops # internal; may change
try:
_group_ops([GetOp(("tenant.bob",), "profile")]) # no database needed
print("NOT FIXED: the store accepted a dotted label")
except InvalidNamespaceError as err:
print("Fixed:", err)On langgraph-checkpoint-postgres 3.1.3, installed on its own, it printed the same Fixed line. In langgraph-checkpoint-postgres 3.1.2, _group_ops only groups operations by type, so it would print NOT FIXED.
A tempting shortcut is to check that python -c "from langgraph.store.base import validate_op_namespace" runs. It fails with ImportError on 4.2.0, but success only proves langgraph-checkpoint is 4.3.0 or newer. On a mixed install it succeeds while the store package is still affected, so rely on the store version and the scripts above.
What if you built your own store or key format?
Then the same bug may be waiting in your code, and the fix has two parts. Validate every operation, not only writes, and prefer a key format that keeps label boundaries.
Validate at the one place every operation passes
If you wrote a custom LangGraph store that builds namespace text with a delimiter, the advisory says to call validate_op_namespace for each operation. Put the call where all operations meet, as the fixed stores do in _group_ops. A check placed in one public method will be skipped by the next method someone adds.
Use an encoding that cannot collide
Rejecting the delimiter works, but it is a rule that every code path must remember. An encoding that keeps boundaries removes the question. Storing the namespace as a JSON array is one simple choice.
import json
def to_key(namespace):
return json.dumps(list(namespace)) # keeps label boundaries
print(to_key(("tenant", "bob")))
print(to_key(("tenant.bob",)))
print(to_key(("tenant", "bob")) == to_key(("tenant.bob",)))What just happened? It printed ["tenant", "bob"], then ["tenant.bob"], then False. The quotes and commas mark where each label ends, and JSON escapes any quote inside a label. Other safe choices are a length prefix before each part, or a real array column such as Postgres text[].
Be careful with home-made escaping. If you replace each dot inside a label with a backslash and a dot, but never escape the backslash itself, collisions return. In a plain Python test, the two labels "a\" (the letter a, then a backslash) and "b" produced the string a\.b. The single label "a.b" produced exactly the same string.
How do you explain the LangGraph store namespace vulnerability in a system design interview?
Use it as a short story about key design and tenant isolation, then generalise. This frame works for any question about multi-tenant storage.
State the bug in one sentence
The SQLite and Postgres stores joined namespace labels with dots, so ("tenant", "bob") and ("tenant.bob",) shared one stored key.
Name the asymmetry
Writes rejected dotted labels but reads, searches, deletes and raw batches did not. A rule on one path is not a rule on the system.
Show the fix
Validate every operation at the single point all of them pass, before any SQL runs. LangGraph did this in 4.3.0 and the matching store releases.
Scope the blast radius
A dotted label only reaches namespaces that share the labels before it. Put the trusted tenant id first so mistakes stay inside one tenant.
Offer a sturdier design
Keep label boundaries in the key, with JSON arrays, length prefixes or array columns, so no input can collide.
Close with defence in depth
Authorise in the same representation the store uses, and add a test that tries a delimiter inside every user-supplied part.
If the interviewer pushes on trade-offs, mention migration. Changing an encoding means rewriting stored keys. Rejection, the route LangGraph took, needs no migration.
Questions people ask
- Is InMemoryStore affected by the LangGraph store namespace vulnerability?
- No. The advisory says InMemoryStore is not affected because it compares namespaces as tuples, so ("tenant", "bob") and ("tenant.bob",) stay different. After upgrading to langgraph-checkpoint 4.3.0, its get, search, delete and listing methods also reject dotted labels, because the new check sits in the shared base class.
- Do I need to upgrade langgraph-checkpoint separately?
- Usually not. langgraph-checkpoint-sqlite 3.1.2 and langgraph-checkpoint-postgres 3.1.3 both require langgraph-checkpoint 4.3.0 or newer, so pip upgrades it with them. Check your lock file, though. A pinned older langgraph-checkpoint will make pip refuse the install rather than quietly skip the fix.
- Will the upgrade break my existing data?
- No migration is needed, because the stored format did not change. The advisory notes that labels written through put could never contain a dot, so no stored data becomes unreachable. What changes is that dotted, empty or non-string labels now raise InvalidNamespaceError on reads, searches, deletes and listings. Code that relied on those calls returning None or an empty list must catch the error instead.
- Was the checkpointer also affected?
- The advisory covers the long-term memory stores in the two packages, not the checkpointers that save thread state. The fix commit changes the store modules in langgraph-checkpoint, langgraph-checkpoint-sqlite and langgraph-checkpoint-postgres. The checkpointer code in these releases changed for unrelated reasons.
- How do I know if my app was exposed?
- The advisory names three conditions: you use one of these stores, you rely on namespaces to keep users or tenants apart, and untrusted input can choose a label. If every label is a fixed string or a server-side id, this path was closed. If a label is untrusted, look at its position. Labels before it decide which namespaces it could reach.
- What about the hosted Agent Server?
- The advisory says the Agent Server rejects these labels from versions 0.15.5 and 0.16.2. It says LangSmith Cloud deployments already include the fix or will receive it on their next revision. Self-hosted deployments should upgrade, as of the advisory's last update on 23 October 2026.
Sources and further reading
- GitHub Advisory GHSA-6hmw-9432-67qq (CVE-2026-115883): namespace labels containing a dot reach other namespaces
- LangGraph pull request 9217: validate namespace labels before store dispatch
- Fix commit bfcfea5 in langchain-ai/langgraph
- OSV record for GHSA-6hmw-9432-67qq (affected ranges and CVSS vector)
- PyPI: langgraph-checkpoint-sqlite 3.1.2
- PyPI: langgraph-checkpoint-postgres 3.1.3
- PyPI: langgraph-checkpoint 4.3.0