FlawOpen

flawopen.com/Teardowns/cve-2026-71433-langgraph-store-namespace-boundary

● CVE-2026-71433 · CVSS 5.3 · Medium
FlawOpen Security Research

LangGraph CVE-2026-71433: Namespace Leak

LangGraph Postgres and SQLite scoped reads could cross namespace boundaries and expose sibling tenant data. Upgrade both checkpoint packages to 3.1.1.

CVSS vectorCVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
💡 Plain English Explainer (ELI5)

Imagine a librarian told to fetch folders beginning with “team.” If the catalog searches raw text, it can return “team2” along with “team.” LangGraph stored each namespace as dot-separated text, then used a prefix pattern that did not require a dot boundary. The fix checks for the exact namespace or a real next segment, so one tenant’s folder is no longer mistaken for another’s.

Core Concepts & Subsystem Terms

Namespace
A tuple of labels used by a LangGraph Store to group items. Applications may use it as a user or tenant boundary.
Prefix scope
A request to read one namespace and its descendants. Correct matching must stop at a segment boundary: foo includes foo.child, not the sibling foobar.
SQL `LIKE` pattern
A text pattern where % matches any sequence and _ matches one character. Those characters must be escaped when namespace labels are meant literally.
CWE-863
Incorrect Authorization: the store returned records outside the namespace the caller was allowed to read.

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-863

Root Cause Analysis

Before 3.1.1, the Postgres and SQLite checkpoint stores flattened namespace tuples with dots and scoped search by applying LIKE '<namespace>%'. LIKE does not understand the namespace separator, so a request scoped to foo could also match sibling foobar; legal % and _ labels could act as wildcards. list_namespaces had related prefix and suffix matching errors. An ordinary scoped read was enough; no malformed input was required. This affects only the Postgres/SQLite store search and namespace-list read paths when applications rely on namespaces for tenant separation. get, put, and delete use exact equality and were not affected; InMemoryStore compares namespace elements and was not affected. The fix matches the exact flattened namespace or requires a dot before descendants, escapes LIKE metacharacters, and uses segment-aware patterns for namespace listing. SQLite also avoids LIKE's ASCII case-insensitive behavior for search. The advisory reports no evidence of exploitation in the wild. The affected packages are fixed in 3.1.1.

CVE-2026-71433 deployment reachability matrix

Deployment and namespace shapeDocumented behaviorTriage action
PostgresStore / AsyncPostgresStore or SqliteStore / AsyncSqliteStore before 3.1.1; tenant isolation depends on namespaces.A scoped search or list_namespaces can cross into a sibling when labels share a textual prefix (foo / foobar, 1 / 12) or contain % / _. SQLite search also treated ASCII case-only differences as equal.Upgrade the relevant checkpoint package to 3.1.1 or later. Test sibling prefixes, wildcard characters, suffix matching, and case-only differences against the production backend.
SQLite store before 3.1.1 with namespace labels differing only in ASCII case.The vulnerable descendant search used case-insensitive LIKE, while exact operations treated case as significant.Upgrade and add a case-sensitivity regression test if labels can differ by case.
Fixed-length UUID namespace labels with no % or _.The advisory says this label scheme is not affected: fixed-length labels cannot be prefixes of one another.Confirm every namespace segment follows that invariant. If labels can vary or contain wildcard characters, treat the deployment as in scope.
InMemoryStore.Not affected: it compares namespace elements individually.No upgrade for this CVE; still enforce application-level tenant ownership checks.
LangSmith hosted deployment.The default LANGGRAPH_STORE_BACKEND=python uses AsyncPostgresStore and is in scope when running an affected package. The separate grpc backend received an equivalent fix.Check the configured backend and deployed version; verify hosted rollout or package version rather than inferring exposure from the product name.

Step-by-Step Attack Flow

Step 1

Check the store backend and labels

The deployment must use langgraph-checkpoint-postgres or langgraph-checkpoint-sqlite before 3.1.1, and rely on namespace scoping for user or tenant separation. Prefix-related labels or % and _ labels make the boundary relevant.

Step 2

A normal scoped read is issued

An authenticated application request searches a namespace such as foo. The caller does not need to submit a special payload; the ordinary namespace scope is the input to the flawed matcher.

Step 3

The backend flattens and pattern-matches

The store joins labels with dots and uses a LIKE prefix ending in %. Because the pattern has no required separator, foo can match foobar; pattern characters in labels can also broaden the match.

Step 4

Sibling namespace items can be returned

Where an application treats namespaces as tenant boundaries, search or namespace listing may return another user's stored items. This is a read-side confidentiality issue, not SQL injection or write access.

Step 5

Upgrade and retest isolation

Upgrade both applicable checkpoint packages to 3.1.1 or later. Test prefix siblings, literal % and _ labels, suffix matching, and the SQLite case-sensitivity behavior using separate test tenants.

Source Code: Flaw vs. Secure Implementation

The vulnerable predicate treats every string beginning with the flattened namespace as a descendant. The patch adds an exact-equality arm and requires a literal dot before descendant segments; it separately escapes backslash, %, and _ for the LIKE arm. The upstream commit also replaces list_namespaces prefix/suffix patterns with segment-aware matching and adjusts SQLite search semantics. This is a source excerpt, not a replacement module.

Source excerpt: libs/checkpoint-postgres/langgraph/store/postgres/base.py — _prepare_batch_search_queries and _namespace_prefix_condition

UNPATCHED FLAW
def _prepare_batch_search_queries(op):
    ns_condition = "TRUE"
    ns_param = None
    if op.namespace_prefix:
        ns_condition = "store.prefix LIKE %s"
        ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
    return ns_condition, ns_param
HARDENED SECURE PATCH
def _escape_like_literal(text: str) -> str:
    return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")

def _namespace_prefix_condition(namespace_prefix: tuple[str, ...]) -> tuple[str, tuple]:
    path = _namespace_to_text(namespace_prefix)
    condition = r"(store.prefix = %s OR store.prefix LIKE %s ESCAPE '\\')"
    return condition, (path, f"{_escape_like_literal(path)}.%")

if op.namespace_prefix:
    ns_condition, ns_param = _namespace_prefix_condition(op.namespace_prefix)

Engineering & System Hardening Checklist

Sources