Severity: high · CWE-863 Incorrect Authorization (scope isolation not enforced) · Location: src/visibility.rs:61 · dim: tenant-isolation
Attack
is_visible_to_caller() short-circuits if scope != "private" { return true }, so scope in {team,unit,org,collective,shared,legacy} passes for ANY caller with no namespace-subtree check. This predicate is the ONLY scope gate on the paths that never invoke the SQL visibility_clause: HTTP GET /api/v1/memories (list_memories, src/handlers/memories_query.rs:148 postgres and :195 sqlite — the code comment itself notes 'db::list does NOT apply the visibility-prefix filter'), the SAL adapters get()/list() (src/store/sqlite.rs:173 and :274; postgres twins), the entity-alias fallback walk (src/handlers/kg.rs:357/383), and the kg_query per-node filter (src/handlers/kg.rs:1384). A co-tenant NHI agent (any authenticated X-Agent-Id, or anonymous when no api_key) sends GET /api/v1/memories?namespace=<victim-org>/<unit>/<team>&limit=1000 and receives every team/unit/org-scoped Memory in the victim's subtree — full title + content + metadata — even though src/models/namespace.rs:42-49 documents Team/Unit/Org as subtree-restricted isolation levels.
Impact
Full-content cross-tenant disclosure of every non-private (team/unit/org/collective) memory across all tenants via a standard list/get endpoint. The private-scope owner fix (#1720) is intact, but the equally-documented team/unit/org boundary provides zero confidentiality on these read paths.
Fix
Route list()/get() (and any is_visible_to_caller call used as the sole scope gate) through the same subtree enforcement the recall/search SQL visibility_clause uses, keyed on the AUTHENTICATED principal's namespace prefixes (never a request-supplied as_agent). Concretely, change is_visible_to_caller to take the caller's (private/team/unit/org) prefixes and apply matches_subtree for Team/Unit/Org instead of returning true, mirroring storage::is_visible().
Found by the v0.9.0 multi-agent security review, adversarially verified. Part of #1918.
Severity: high · CWE-863 Incorrect Authorization (scope isolation not enforced) · Location:
src/visibility.rs:61· dim: tenant-isolationAttack
is_visible_to_caller() short-circuits
if scope != "private" { return true }, so scope in {team,unit,org,collective,shared,legacy} passes for ANY caller with no namespace-subtree check. This predicate is the ONLY scope gate on the paths that never invoke the SQL visibility_clause: HTTP GET /api/v1/memories (list_memories, src/handlers/memories_query.rs:148 postgres and :195 sqlite — the code comment itself notes 'db::list does NOT apply the visibility-prefix filter'), the SAL adapters get()/list() (src/store/sqlite.rs:173 and :274; postgres twins), the entity-alias fallback walk (src/handlers/kg.rs:357/383), and the kg_query per-node filter (src/handlers/kg.rs:1384). A co-tenant NHI agent (any authenticated X-Agent-Id, or anonymous when no api_key) sendsGET /api/v1/memories?namespace=<victim-org>/<unit>/<team>&limit=1000and receives every team/unit/org-scoped Memory in the victim's subtree — full title + content + metadata — even though src/models/namespace.rs:42-49 documents Team/Unit/Org as subtree-restricted isolation levels.Impact
Full-content cross-tenant disclosure of every non-private (team/unit/org/collective) memory across all tenants via a standard list/get endpoint. The private-scope owner fix (#1720) is intact, but the equally-documented team/unit/org boundary provides zero confidentiality on these read paths.
Fix
Route list()/get() (and any is_visible_to_caller call used as the sole scope gate) through the same subtree enforcement the recall/search SQL visibility_clause uses, keyed on the AUTHENTICATED principal's namespace prefixes (never a request-supplied as_agent). Concretely, change is_visible_to_caller to take the caller's (private/team/unit/org) prefixes and apply matches_subtree for Team/Unit/Org instead of returning true, mirroring storage::is_visible().
Found by the v0.9.0 multi-agent security review, adversarially verified. Part of #1918.