Skip to content

Python OSS SDK: add() lets metadata set identity scope (user_id/agent_id/run_id/actor_id) on creation #6655

Description

@OfficialAbhinavSingh

Component

Python SDK

Summary

The Python OSS SDK's add() has the same identity-field bug class as update() (fixed in #6278 for #6277), but on the creation path: caller-supplied metadata can set a memory's identity scope (user_id / agent_id / run_id / actor_id) whenever the matching top-level entity param is not also passed.

This is the Python twin of #6371 (TypeScript add(), PR #6377). update() is already hardened on both sides; add() is not hardened on either.

Where

mem0/memory/main.py, _build_filters_and_metadata():

base_metadata_template = deepcopy(input_metadata) if input_metadata else {}
...
if user_id:
    base_metadata_template["user_id"] = user_id
if agent_id:
    base_metadata_template["agent_id"] = agent_id
if run_id:
    base_metadata_template["run_id"] = run_id

The caller's metadata is deep-copied wholesale into the stored payload template, and the identity keys are re-pinned only when the corresponding argument is truthy. So for any identity key the caller does not pass as a top-level argument, whatever value was in metadata survives into the created memory. actor_id is never re-pinned here at all.

The repo already has the right helper for this, _strip_identity_keys() with the module-level _IDENTITY_KEYS = ENTITY_PARAMS | {"actor_id"}, but it is only called from the two _update_memory paths, never from the creation path.

Steps to Reproduce

from mem0.memory.main import _build_filters_and_metadata

metadata, _ = _build_filters_and_metadata(
    user_id="attacker",
    input_metadata={"agent_id": "victim-agent", "run_id": "victim-run", "actor_id": "victim-actor"},
)
print(metadata)

End to end through the public API, with the vector store mocked to capture the payload:

memory.add(
    "I like coffee",
    user_id="attacker",
    metadata={"agent_id": "victim-agent", "run_id": "victim-run"},
    infer=False,
)

Expected Behavior

Identity scope is set only through the documented top-level user_id / agent_id / run_id params, never through freeform metadata, matching what update() already enforces.

Actual Behavior

Both reproduce the injection:

# helper:
{'agent_id': 'victim-agent', 'run_id': 'victim-run', 'actor_id': 'victim-actor', 'user_id': 'attacker'}

# through Memory.add(), payload handed to the vector store:
{'agent_id': 'victim-agent', 'run_id': 'victim-run', 'user_id': 'attacker'}

The created memory is now discoverable through get_all(filters={"agent_id": "victim-agent"}) and search(..., filters={"run_id": "victim-run"}), scopes the caller never held.

For contrast, the same input on the update path is correctly rejected:

update(): ignoring metadata['user_id'] - identity fields are immutable after creation
update(): ignoring metadata['agent_id'] - identity fields are immutable after creation
-> {'note': 'ok'}

Note an explicitly passed argument does still win over metadata for the same key (user_id="attacker" plus metadata={"user_id": "victim"} stores attacker), so the gap is exactly the keys the caller leaves unset.

Impact

Same tenant-isolation class as #6277 / #6342, on the creation path rather than update. It is a cross-scope write: an application that forwards user-controlled metadata into add() lets a caller place memories into scopes it does not own. Both sync Memory.add() and AsyncMemory.add() are affected, on both infer=True and infer=False, since the template is deep-copied into every per-memory payload.

Environment

  • mem0 version: 2.0.14 (reproduced on main @ ea2ee07)
  • Python version: 3.12
  • OS: Linux

Suggested Fix

Apply the existing _IDENTITY_KEYS filter to the incoming metadata in _build_filters_and_metadata() before it becomes the payload template, so identity scope can only be set via the top-level params. That mirrors the update() fix (#6278) and the TS add() fix proposed in #6371, and keeps a single source of truth for the key list.

Happy to open the PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions