Agentic AI · BraivIQ AI Engineering Playbook
Agent Memory Isolation: What The October 2026 Mem0 And LangGraph Advisories Teach About Multi-Tenant AI Agents
Agent memory isolation fails in small places: a key built by joining strings, a handler that rewrites a namespace instead of refusing it, an ID dropped into a URL without encoding. Three advisories published on 28 October 2026 show each one. Mem0 2.0.18 fixed a cross-tenant leak caused by unescaped delimiters, langgraph-api 0.15.0 fixed cross-user Store access through rewritten namespaces, and langgraph-sdk 0.4.6 fixed path redirection through unencoded IDs. This playbook shows the fixes as tested Python: hashed canonical scope keys, reject-only namespace checks, encoded path segments and an audit log of every refusal.
Published · Updated · 13 min read · By BraivIQ Engineering
Key takeaways
- Mem0 2.0.18 (advisory published 28 October 2026, CVE-2026-116575) fixed a scope key built by joining user_id, agent_id and run_id with unescaped "&" and "=". A caller who controlled run_id could produce another user's key.
- langgraph-api 0.15.0 (CVE-2026-118668, high) fixed JavaScript and TypeScript Store handlers that rewrote a requested namespace. Handlers that rejected the request instead were not affected.
- langgraph-sdk 0.4.6 (CVE-2026-118667) fixed thread and assistant IDs placed into request paths without encoding.
- Build scope keys from canonical JSON and hash them, refuse any namespace outside the caller's own, encode every path segment and log every refusal.
- Treat cross-tenant sharing as an action a named person approves, never a side effect of how a key is built.
3 - Memory or namespace isolation advisories published on 28 October 2026 (Mem0, langgraph-api, langgraph-sdk) · 6.3 - CVSS score of CVE-2026-116575 in Mem0, fixed in 2.0.18 · 0.15.0 - langgraph-api version that fixes cross-user Store access (CVE-2026-118668, high)
Agent memory isolation means one tenant's agent can never read or write another tenant's memory, whatever identifiers it sends. In financial firms that memory holds client conversations, onboarding notes and trade context, so a leak is a data breach. The rule our Agentic AI London team builds to is simple: derive keys so callers cannot forge them, check every request against the caller's own namespace and refuse anything else, and record each refusal.
The two samples run on Python 3.14.4 with the standard library only. All identifiers are made up.
What did the 28 October advisories disclose?
| Advisory | Package and fix | What went wrong |
|---|---|---|
| CVE-2026-116575 (CVSS 6.3) | mem0ai, fixed in 2.0.18 | The session scope key joined user_id, agent_id and run_id with "&" and "=". Values were never checked for those characters. |
| CVE-2026-118668 (high) | langgraph-api, fixed in 0.15.0 | JavaScript or TypeScript Store handlers that rewrote the requested namespace could let an ordinary user reach other users' data. |
| CVE-2026-118667 (CVSS 4.3) | langgraph-sdk, fixed in 0.4.6 | thread_id and assistant_id were put into request paths without encoding, so relative segments could redirect an authenticated request. |
The Mem0 advisory explains the impact plainly. Applications often pass a client or session identifier straight through as run_id, so any caller who controls that value can craft one containing "&user_id=" followed by another user's ID. The langgraph-api advisory notes that Python handlers were not affected, and that JavaScript or TypeScript handlers which reject unauthorised namespace requests, rather than rewriting them, were also safe.
What does agent memory isolation look like as an architecture?
Four controls, each with one job. The scope key is derived, never assembled from text. The namespace check compares the requested namespace with the caller's identity from your sign-in system and refuses a mismatch. Identifiers are encoded before they reach a URL. Sharing across tenants, which is sometimes legitimate, goes through a named reviewer instead of a clever key.
How do you build a scope key a caller cannot forge?
memory_scope.py"""Scope keys for agent memory that one tenant cannot forge into another's.
CVE-2026-116575 (Mem0, fixed in 2.0.18, 28 October 2026): the session scope key was built by joining
user_id, agent_id and run_id with unescaped "&" and "=", so a caller who controlled run_id could make
a key equal to another user's. Python 3.14 standard library only. Sample identifiers only.
"""
from __future__ import annotations
import hashlib
import json
def naive_scope(filters: dict[str, str]) -> str:
"""The vulnerable shape: string joining with delimiters the caller can type."""
return "&".join(f"{k}={filters[k]}" for k in sorted(["user_id", "agent_id", "run_id"]) if filters.get(k))
def safe_scope(tenant: str, filters: dict[str, str]) -> str:
"""Canonical JSON then SHA-256: no delimiter in any value can change the structure."""
for key in ("user_id", "agent_id", "run_id"):
value = filters.get(key, "")
if any(ord(ch) < 32 for ch in value):
raise ValueError(f"{key} contains control characters")
body = json.dumps({"tenant": tenant, **{k: filters.get(k, "") for k in ("user_id", "agent_id", "run_id")}},
sort_keys=True, separators=(",", ":"))
return hashlib.sha256(body.encode()).hexdigest()
if __name__ == "__main__":
victim = {"agent_id": "ops-agent", "run_id": "x", "user_id": "client-0042"}
# The attacker controls only their own run_id and sends no user_id of their own.
attacker = {"agent_id": "ops-agent", "run_id": "x&user_id=client-0042"}
print("naive victim key: ", naive_scope(victim))
print("naive attacker key:", naive_scope(attacker))
print("naive collision: ", naive_scope(victim) == naive_scope(attacker))
print("safe collision: ", safe_scope("firm-a", victim) == safe_scope("firm-a", attacker))
print("same ids, other tenant:", safe_scope("firm-a", victim) == safe_scope("firm-b", victim))Python 3.14.4, standard library only. Run once as shown. Sample identifiers only.
The naive builder reproduces the Mem0 pattern. A victim session with run_id "x" and an attacker who sends run_id "x&user_id=client-0042" and no user_id of their own produce the same key, so the attacker reads the victim's last messages. The safe builder serialises the fields as canonical JSON, adds the tenant, rejects control characters and hashes the result. The collision disappears, and the same user IDs under another tenant give a different key.
How should a namespace check behave?
store_guard.py"""Namespace checks for a shared agent memory store: reject, never rewrite, and encode identifiers.
CVE-2026-118668 (langgraph-api, fixed in 0.15.0): JavaScript or TypeScript handlers that rewrote the
requested namespace could let a user read outside it. Handlers that rejected instead were not affected.
CVE-2026-118667 (langgraph-sdk, fixed in 0.4.6): IDs interpolated into request paths without encoding.
Python 3.14 standard library only. Sample data only.
"""
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from urllib.parse import quote
AUDIT: list[dict[str, str]] = []
def audit(**event: str) -> None:
prev = AUDIT[-1]["hash"] if AUDIT else "0" * 64
body = {**event, "at": datetime.now(timezone.utc).isoformat(), "prev": prev}
AUDIT.append({**body, "hash": hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()})
def authorise(caller_tenant: str, caller_user: str, namespace: tuple[str, ...]) -> tuple[str, ...]:
"""Allow only the caller's own namespace. Anything else is refused and logged, not quietly rewritten."""
if namespace[:2] != (caller_tenant, caller_user):
audit(event="refused", tenant=caller_tenant, user=caller_user, asked="/".join(namespace))
raise PermissionError(f"{caller_user} may not read {'/'.join(namespace)}")
audit(event="allowed", tenant=caller_tenant, user=caller_user, asked="/".join(namespace))
return namespace
def thread_path(thread_id: str) -> str:
"""Encode every path segment, including '/', so '../' cannot move the request elsewhere."""
return f"/threads/{quote(thread_id, safe='')}/state"
if __name__ == "__main__":
print(authorise("firm-a", "analyst-7", ("firm-a", "analyst-7", "notes")))
try:
authorise("firm-a", "analyst-7", ("firm-a", "analyst-9", "notes"))
except PermissionError as exc:
print("refused:", exc)
print("raw path: ", "/threads/" + "../assistants/admin" + "/state")
print("encoded path:", thread_path("../assistants/admin"))
for e in AUDIT:
print(f" {e['event']:<8} {e['asked']:<26} {e['hash'][:12]}")Python 3.14.4, standard library only. Run once as shown. Sample data only.
authorise() compares the first two namespace parts with the caller's tenant and user and raises on any difference. It does not quietly swap in the caller's own namespace, which is the behaviour that turned into CVE-2026-118668. Every allow and refusal is chained into the audit log. thread_path() encodes the identifier with no safe characters, so "../assistants/admin" becomes a single harmless segment.
What breaks in production?
- **Keys migrated in place.** Changing the key format strands existing memory. Write both keys during a migration window, read the new one first and delete old keys on a schedule.
- **Identity taken from the request body.** The tenant and user must come from the authenticated session, never from fields the client sends.
- **Search that skips the check.** Vector or keyword search across memory must filter by namespace inside the query, not after results come back.
- **Shared agents.** An agent that serves many clients needs a run-time tenant on every call. A default tenant hides mistakes.
- **Logs that hold the data.** Log namespaces and record IDs, not memory contents.
What are the alternatives to building isolation yourself?
| Option | Good fit | Trade-off |
|---|---|---|
| One store per tenant | Few, large tenants with strict separation | More infrastructure, simplest to reason about |
| Shared store with row-level security in the database | Many tenants on a relational store | Isolation enforced below the application, but policies need tests |
| Framework memory with patched versions | Prototypes and internal tools | You rely on the framework getting every path right |
| Shared store with application checks, as above | Most agent platforms | You own the tests, and every new access path needs one |
Our playbook on prompt injection defence covers what happens when untrusted text gets into memory, and approvals that hold up in AI agent code covers the approval store. For the data protection side, see our blog on the ICO's agentic AI call for evidence.
Where should a team start?
Upgrade the three packages if you use them, then write one test per access path that tries another tenant's namespace and an identifier full of delimiters and "../". Our AI developers in London build these tests into every agent, for example on client onboarding files, and the 14-day Proof Run starts with the data map and the isolation tests.
Frequently asked questions
What is agent memory isolation?
It is making sure an AI agent serving one tenant or user can never read or write memory belonging to another, whatever identifiers it is given. It relies on keys callers cannot forge, a namespace check on every request and a record of refusals.
Which versions fix the October 2026 memory advisories?
mem0ai 2.0.18 fixes CVE-2026-116575, langgraph-api 0.15.0 fixes CVE-2026-118668 and langgraph-sdk 0.4.6 fixes CVE-2026-118667, according to their advisories published on 28 October 2026.
Should a namespace check rewrite or reject a request?
Reject it. The langgraph-api advisory notes that handlers which rejected unauthorised namespace requests were not affected, while handlers that rewrote the namespace could expose other users' data.
Is hashing the scope key enough on its own?
No. Hashing canonical JSON stops delimiter tricks, but you still need the tenant and user from the authenticated session, a namespace check on every read and write, and tests for each access path.
References
- GitHub Advisory Database, "Mem0: cross-tenant conversation leak via unescaped delimiter collision in session-scope keys (CVE-2026-116575)", 28 October 2026. https://github.com/advisories/GHSA-5324-3w58-qpp2
- GitHub Advisory Database, "LangGraph JavaScript custom authentication may permit cross-user Store access (CVE-2026-118668)", 28 October 2026. https://github.com/advisories/GHSA-4hm6-w6qq-w73v
- GitHub Advisory Database, "LangGraph SDK v3 streaming: unencoded identifiers can redirect authenticated requests (CVE-2026-118667)", 28 October 2026. https://github.com/advisories/GHSA-3fx2-cqw9-xr3c
- Information Commissioner's Office, "The ICO's call for evidence on agentic AI", 8 October 2026. https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/artificial-intelligence/the-ico-s-call-for-evidence-on-agentic-ai/