GHSA-5324-3w58-qpp2: Infoleak
Summary
buildsessionscope() in mem0/memory/main.py builds the conversation-history scope key used to fetch/store the last N raw messages by naively joining caller-supplied userid/agentid/runid values with unescaped & and = delimiters:
def buildsessionscope(filters): parts = [] for key in sorted(["userid", "agentid", "runid"]): val = filters.get(key) if val: parts.append(f"{key}={val}") return "&".join(parts)
validateandtrimentityid() (the only validation applied to these values) rejects only empty/whitespace-only strings — it never forbids & or =. This scope string is then used as an exact-match key against a SQLite column (storage.py: WHERE sessionscope = ?) to fetch the last 10 raw messages for a given scope (main.py, Phase 0 of addtovectorstore, both sync and async).
Impact
Any caller who controls only their own runid (or agentid) — a very common integration shape, since apps frequently pass a client-/session-supplied identifier straight through as runid without namespacing — can craft a value containing &userid=<victim> (or &agentid=<victim>) that produces a scope string byte-for-byte identical to a different tenant's real scope string.
Concretely: Tenant A calls add(msgs, userid="alice", runid="proj-x") → scope "runid=proj-x&userid=alice". An attacker who can only set runid calls add(msgs, runid="proj-x&userid=alice") → identical scope string. The attacker's next add() call:
1. Reads Tenant A's raw prior conversation via getlastmessages(sessionscope) and splices it verbatim into the attacker's own LLM fact-extraction prompt — a direct cross-tenant confidentiality leak (conversation content, potentially including PII, credentials, etc.). 2. Writes into the same colliding row-set via savemessages(), whose eviction logic (DELETE FROM messages WHERE sessionscope = ? AND id NOT IN (...)) can evict/interleave Tenant A's history — permanent cross-tenant data corruption, not just a read.
Reachable via the public Memory.add() / AsyncMemory.add() API, and by extension the REST server's POST /memories endpoint (which takes runid/agentid straight from the request body).
Reproduction
from mem0.memory.main import buildsessionscope
Tenant A: real userid + runid scopea = buildsessionscope({"userid": "alice", "runid": "proj-x"}) print(scopea) # "runid=proj-x&userid=alice"
Attacker: controls only runid, embeds a fake userid inside it scopec = buildsessionscope({"runid": "proj-x&userid=alice"}) print(scopec) # "runid=proj-x&userid=alice"
assert scopea == scopec # byte-for-byte collision
A full end-to-end proof (actual add() calls, no mocking of the vulnerable functions) confirms the attacker's subsequent add() call receives the victim's raw prior message content via getlastmessages.
Suggested Fix
Reject & and = in validateandtrimentityid(), alongside the existing whitespace check — this is the single validation chokepoint already used by every entry point (add, search, getall, deleteall), so the fix is centralized. Alternatively/additionally, encode each component unambiguously (percent-encode each id, or use a length-prefixed/JSON-encoded scope key) so no valid-character combination in one field can ever collide with a different partition.
Also affected: TypeScript SDK (npm mem0ai)
The same flaw exists in mem0-ts/src/oss/src/memory/index.ts, Memory.buildSessionScope(), which joins agentid/runid/userid into the same key=value&key=value string with the delimiters unescaped:
private buildSessionScope(filters: SearchFilters): string { const parts: string[] = []; for (const key of ["agentid", "runid", "userid"].sort()) { const val = (filters as any)[key]; if (val) parts.push(${key}=${val}); } return parts.join("&"); }
The scope string is used the same way, as an exact-match key into the messages table via SQLiteManager.getLastMessages() and saveMessages(), so the identical forge, cross-tenant read and eviction all apply.
Verified against the current releases as of 2026-07-26: pip mem0ai v2.0.14 and npm mem0ai ts-v3.1.2 both still contain the unescaped join.
Reporter: Abhinav Singh (GitHub: OfficialAbhinavSingh)
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/mem0aito a version that resolves this vulnerability.Fixed in 3.1.6 - Upgrade
Upgrade
pip/mem0aito a version that resolves this vulnerability.Fixed in 2.0.18 - Configuration
Update the centralized entity-ID validation used by add, search, get_all, and delete_all to reject '&' and '=' alongside the existing whitespace checks, preventing caller-supplied run_id, agent_id, or user_id values from colliding in the session scope key.
mem0 Memory API and TypeScript SDK _validate_and_trim_entity_id() validation = Reject '&' and '=' characters
Event History
Frequently Asked Questions
Which deployments are most exposed?
Deployments that pass client- or session-supplied identifiers directly as run_id or agent_id without namespacing are especially exposed. The affected scope is used to retrieve and store the last 10 raw messages in SQLite.
What does an attacker need to exploit this?
An attacker needs control over their own run_id or agent_id and must craft an identifier containing delimiter characters such as & or = to collide with another tenant's scope. The supplied vector has local attack access, low privileges, high complexity, and requires no user interaction.
What is the practical impact of a successful collision?
A colliding scope can cause operations to target another tenant's conversation-history scope, including retrieval of that scope's last 10 raw messages. The advisory rates confidentiality and integrity impact as high.
How can I determine whether my integration is affected?
Review whether user_id, agent_id, or run_id values can contain & or = and whether these values originate from callers without application-controlled namespacing or escaping. The described validation rejects only empty or whitespace-only identifiers, not delimiter characters.
Is this an SQL injection through the SQLite lookup?
The described SQLite lookup uses an exact-match parameterized condition, WHERE session_scope = ?. The issue described is a session-scope key collision caused by unescaped delimiters, rather than SQL text being injected into the query.