You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two SQL-safety bugs in the data layer: an identifier interpolated without an allowlist, and unescaped LIKE wildcards on caller-influenced text.
Findings
increment_recall_counts interpolates the table name (high)
store.py:2060-2071 — table is interpolated into SQL via f-string with no allowlist. It's a public method on MemoryStore; a caller passing a derived table name gets arbitrary SQL (table = "memory_entries; DROP TABLE memory_entries; --" runs as a multi-statement via psycopg). Internal callers pass literals today, but nothing enforces it.
Fix:assert table in {"memory_entries","conversations","delegations"}.
replace()/remove() don't escape LIKE wildcards (medium)
store.py:917, 942, 974 — build LIKE '%' || old_text || '%' without escaping %/_. old_text comes from memory-tool arguments (conversation-influenced).
Scenario:remove(old_text="%") deletes every entry in the (agent, target) scope; _ matches any character, so replaces/removes hit unintended rows.
store.py:1298-1315, 2024-2058 — fetch_full, confirm_entry, reject_entry take a bare sequential id with no agent_identity filter. (Cross-referenced from the security issue; fix location is the store layer.)
Suggested direction
Allowlist the table identifier; escape LIKE metacharacters; add agent_identity scoping to by-id reads/mutations.
PR #28 closed the network-surface parts of #13 — HTTP bearer-token auth (HEXUS_API_TOKEN), loopback-by-default bind, gated /metrics, and the memory_cleanup dry-run/confirm gate. #13 is now closed; the remaining, deeper isolation work lands here (this issue already owns the "unscoped by-id ops" item, which is the same theme).
Remaining items now tracked in this issue:
A. Agent identity is client-asserted (high/critical).X-Hermes-Session-Key is never read by the MCP server; every tool takes agent_identity as a plain caller-chosen string. Even an authenticated client can pass any identity. Fix: derive agent_identity server-side from the authenticated credential (map bearer token / session key → identity) and treat the tool argument as a scope hint within the caller's own identity, not a free choice. This is the root cause behind the by-id scoping below.
B. By-id reads/mutations are unscoped (already listed here).store.fetch_full (memory_retrieve/headroom_retrieve), confirm_entry/reject_entry (memory_confirm/memory_reject), and memory_summarize_session operate on a bare sequential BIGSERIAL id with no agent_identity filter — enumerable cross-agent read/mutate. Add agent_identity scoping to these store methods and thread the caller's identity through the tools.
C. agent_identity default asymmetry (medium). Empty agent_identity means "this agent" in memory_search/memory_count (tools.py:291,476) but "ALL agents" in memory_recall (tools.py:232-234). Make the empty-identity semantics consistent across tools (security-relevant: the "all agents" default is a silent cross-agent read).
Together with the existing SQL-safety items in this issue (table-identifier allowlist, LIKE-wildcard escaping), this issue now covers "enforce real multi-agent isolation at the data layer."
Activity
codenamekt commentedon Jul 7, 2026
Carried over from #13 (security follow-ups)
PR #28 closed the network-surface parts of #13 — HTTP bearer-token auth (
HEXUS_API_TOKEN), loopback-by-default bind, gated/metrics, and thememory_cleanupdry-run/confirm gate. #13 is now closed; the remaining, deeper isolation work lands here (this issue already owns the "unscoped by-id ops" item, which is the same theme).Remaining items now tracked in this issue:
A. Agent identity is client-asserted (high/critical).
X-Hermes-Session-Keyis never read by the MCP server; every tool takesagent_identityas a plain caller-chosen string. Even an authenticated client can pass any identity. Fix: deriveagent_identityserver-side from the authenticated credential (map bearer token / session key → identity) and treat the tool argument as a scope hint within the caller's own identity, not a free choice. This is the root cause behind the by-id scoping below.B. By-id reads/mutations are unscoped (already listed here).
store.fetch_full(memory_retrieve/headroom_retrieve),confirm_entry/reject_entry(memory_confirm/memory_reject), andmemory_summarize_sessionoperate on a bare sequential BIGSERIAL id with noagent_identityfilter — enumerable cross-agent read/mutate. Addagent_identityscoping to these store methods and thread the caller's identity through the tools.C.
agent_identitydefault asymmetry (medium). Emptyagent_identitymeans "this agent" inmemory_search/memory_count(tools.py:291,476) but "ALL agents" inmemory_recall(tools.py:232-234). Make the empty-identity semantics consistent across tools (security-relevant: the "all agents" default is a silent cross-agent read).Together with the existing SQL-safety items in this issue (table-identifier allowlist, LIKE-wildcard escaping), this issue now covers "enforce real multi-agent isolation at the data layer."
Merge pull request #29 from codenamekt/fix/multi-agent-isolation