GHSA-5324-3w58-qpp2: Infoleak

Published Oct 28, 2026
·
Updated

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

2 affected componentsFixes available
npm/mem0ai<3.1.6
3.1.6
pip/mem0ai<2.0.18
2.0.18

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/mem0ai to a version that resolves this vulnerability.

    Fixed in 3.1.6
  2. Upgrade

    Upgrade pip/mem0ai to a version that resolves this vulnerability.

    Fixed in 2.0.18
  3. 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

Oct 28, 2026
Advisory Published
via GitHub·09:17 PM
Data Sourced
via GitHub·09:17 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

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.

2

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.

3

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.

4

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.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203