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.
Component
Python SDK
Summary
The Python OSS SDK's
add()has the same identity-field bug class asupdate()(fixed in #6278 for #6277), but on the creation path: caller-suppliedmetadatacan 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():The caller's
metadatais 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 inmetadatasurvives into the created memory.actor_idis 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_memorypaths, never from the creation path.Steps to Reproduce
End to end through the public API, with the vector store mocked to capture the payload:
Expected Behavior
Identity scope is set only through the documented top-level
user_id/agent_id/run_idparams, never through freeformmetadata, matching whatupdate()already enforces.Actual Behavior
Both reproduce the injection:
The created memory is now discoverable through
get_all(filters={"agent_id": "victim-agent"})andsearch(..., filters={"run_id": "victim-run"}), scopes the caller never held.For contrast, the same input on the update path is correctly rejected:
Note an explicitly passed argument does still win over
metadatafor the same key (user_id="attacker"plusmetadata={"user_id": "victim"}storesattacker), 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 syncMemory.add()andAsyncMemory.add()are affected, on bothinfer=Trueandinfer=False, since the template is deep-copied into every per-memory payload.Environment
main@ ea2ee07)Suggested Fix
Apply the existing
_IDENTITY_KEYSfilter 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 theupdate()fix (#6278) and the TSadd()fix proposed in #6371, and keeps a single source of truth for the key list.Happy to open the PR.