---
id: CVE-2026-116575
aliases:
  - GHSA-5324-3w58-qpp2
title: >-
  Mem0: Cross-tenant conversation leak via unescaped delimiter collision in
  session-scope key (_build_session_scope)
summary: >-
  Mem0: Cross-tenant conversation leak via unescaped delimiter collision in
  session-scope key (_build_session_scope)
severity: medium
cvss: 6.3
cvssVector: 'CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N'
vendor: mem0ai
product: mem0ai
ecosystem: pip
affected:
  - mem0ai < 2.0.18
  - mem0ai < 3.1.6
patched:
  - mem0ai 2.0.18
  - mem0ai 3.1.6
published: '2026-10-28'
updated: '2026-10-28'
sourceUpdated: '2026-10-28T21:30:05.847356896Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-5324-3w58-qpp2'
references:
  - url: 'https://github.com/mem0ai/mem0/security/advisories/GHSA-5324-3w58-qpp2'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-116575'
  - url: 'https://github.com/mem0ai/mem0/pull/6892'
  - url: >-
      https://github.com/mem0ai/mem0/commit/71f2ebefa3494da21550fb525216818776cde67f
  - url: 'https://github.com/mem0ai/mem0'
  - url: 'https://github.com/mem0ai/mem0/releases/tag/ts-v3.1.6'
  - url: 'https://github.com/mem0ai/mem0/releases/tag/v2.0.18'
  - url: 'https://github.com/advisories/GHSA-5324-3w58-qpp2'
tags:
  - osv
  - pip
  - ghsa
  - cve.org
epss: 0.00108
epssPercentile: 0.01188
ssvc:
  exploitation: none
  automatable: 'no'
  technicalImpact: total
  timestamp: '2026-10-27T13:45:14.377647Z'
cwe:
  - CWE-200
  - CWE-863
ingestedAt: '2026-10-26T20:39:42.618Z'
---

## Overview

Summary

_build_session_scope() 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 user_id/agent_id/run_id values with unescaped & and = delimiters:

def _build_session_scope(filters):
    parts = []
    for key in sorted(["user_id", "agent_id", "run_id"]):
        val = filters.get(key)
        if val:
            parts.append(f"{key}={val}")
    return "&".join(parts)

_validate_and_trim_entity_id() (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 session_scope = ?) to fetch the last 10 raw messages for a given scope (main.py, Phase 0 of _add_to_vector_store, both sync and async).

Impact

Any caller who controls only their own run_id (or agent_id) — a very common integration shape, since apps frequently pass a client-/session-supplied identifier straight through as run_id without namespacing — can craft a value containing &user_id=<victim> (or &agent_id=<victim>) that produces a scope string byte-for-byte identical to a different tenant's real scope string.

Concretely: Tenant A calls add(msgs, user_id="alice", run_id="proj-x") → scope "run_id=proj-x&user_id=alice". An attacker who can only set run_id calls add(msgs, run_id="proj-x&user_id=alice") → identical scope string. The attacker's next add() call:

1. Reads Tenant A's raw prior conversation via get_last_messages(session_scope) 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 save_messages(), whose eviction logic (DELETE FROM messages WHERE session_scope = ? 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 run_id/agent_id straight from the request body).

Reproduction

from mem0.memory.main import _build_session_scope

# Tenant A: real user_id + run_id
scope_a = _build_session_scope({"user_id": "alice", "run_id": "proj-x"})
print(scope_a)  # "run_id=proj-x&user_id=alice"

# Attacker: controls only run_id, embeds a fake user_id inside it
scope_c = _build_session_scope({"run_id": "proj-x&user_id=alice"})
print(scope_c)  # "run_id=proj-x&user_id=alice"

assert scope_a == scope_c  # 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 get_last_messages.

Suggested Fix

Reject & and = in _validate_and_trim_entity_id(), alongside the existing whitespace check — this is the single validation chokepoint already used by every entry point (add, search, get_all, delete_all), 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 agent_id/run_id/user_id into the same
`key=value&key=value` string with the delimiters unescaped:

  private buildSessionScope(filters: SearchFilters): string {
    const parts: string[] = [];
    for (const key of ["agent_id", "run_id", "user_id"].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 packages

- `mem0ai < 2.0.18`
- `mem0ai < 3.1.6`

## Remediation

Upgrade to a patched release:

- `mem0ai 2.0.18`
- `mem0ai 3.1.6`
