The Key Was a Query String: Mem0's Unescaped Delimiters Leaked One Tenant's Agent Memory to Another

Agent memory is becoming the crown jewels — every deployment that gives an assistant a past gives it something worth stealing. CVE-2026-116575 (GitHub advisory GHSA-5324-3w58-qpp2, CVSS 6.3), published to NVD on 26 October 2026 with the advisory following on 28 October, shows how little it takes to break the wall between tenants: Mem0's session-scope key was built by concatenating caller-controlled identifiers with raw = and & delimiters and never escaping them. Any caller who controlled a single identifier — typically just run_id — could forge a scope string byte-identical to another tenant's and read, poison, or evict that tenant's recent conversation history.

The vulnerable construction lived in _build_session_scope() in mem0/memory/main.py (and its TypeScript twin Memory.buildSessionScope()user_id, agent_id, run_id it emitted key=value pairs joined by &. The only validation, _validate_and_trim_entity_id(), rejected empty or whitespace-only strings — it never forbade the structural characters. That string then served as the exact-match key (WHERE session_scope = ?) for fetching the last raw messages and appending new ones, in both sync and async paths and through the REST server's POST /memories endpoint, which takes run_id and agent_id straight from the request body. Parameterized SQL stopped SQL injection dead, and it did not matter: the collision happens before the query, in the key itself.

One identifier is enough

The advisory's reproduction is brutally short. Tenant A calls add() with user_id="alice", run_id="proj-x" and gets scope run_id=proj-x&user_id=alice. An attacker who controls only run_id calls add() with run_id="proj-x&user_id=alice" and gets the identical string. From there the attacker's next add() does two things at once: it reads the victim's raw prior conversation via get_last_messages() and splices it verbatim into the attacker's own LLM fact-extraction prompt — a direct confidentiality breach carrying whatever the victim discussed, credentials and PII included — and it writes into the same row set, where the eviction logic (DELETE FROM messages WHERE session_scope = ? AND id NOT IN (...)) can interleave or permanently evict the victim's history. Read, poison, and corrupt, all through the public Memory.add() API, no privileges beyond a normal tenant account. That is the shape CWE-863 (incorrect authorization) and CWE-200 (exposure to the wrong actor) take when the authorization decision is a string comparison against an attacker-influenced string.

Why this pattern keeps recurring

This is the same class of mistake this site keeps documenting at the boundary where agent frameworks meet storage: the framework treats a composite key as an opaque string and forgets that one of its components is attacker-controlled. AfterDeleteBench showed agent memory surviving deletion; Log-To-Leak showed post-hoc MCP logging becoming a prompt-injection channel. Mem0 adds the multi-tenant dimension: memory infrastructure borrowed query-string serialization without query-string escaping, violating the one property composite keys must have — two distinct inputs must never produce the same output. The fix, in Python SDK 2.0.18 and TypeScript SDK 3.1.6, is delimiter escaping; anything pinned below those versions in a multi-tenant deployment should be treated as sharing one memory.

What to do

  • Upgrade past the fix. Python mem0ai 2.0.18+ and npm mem0ai 3.1.6+. Current release lines (2.2.x / 3.3.x) already contain it — the risk sits with pinned older versions.
  • Audit multi-tenant identifier flow. If your application passes client- or session-supplied strings straight through as run_id or agent_id without namespacing, that was the exploitable shape. Namespace identifiers per tenant server-side rather than trusting caller-supplied values.
  • Assume historical exposure on shared deployments. The flaw allows silent reads spliced into another tenant's extraction prompt — there is no error, no failed query, nothing to alert on. If untrusted callers shared a Mem0 backend below the fixed versions, review what lived in recent conversation buffers.
  • Canonicalize composite keys everywhere. Any key=value&key=value serialization of mixed-trust inputs needs escaping or length-prefixing. Grep for the pattern, not just this instance.

Sources: