Last full audit: 2026-06-24 · Audited version: 1.4.65
Scope: Full source tree — src/ (FastAPI service, MCP tools, services, DB, graph),
memory_mcp.py, cloudflare-worker/src/index.ts, hook templates.
This document is the durable record of YourMemory's security review. It maps the codebase to the relevant SOC 2 Trust Service Criteria, records every vulnerability found and remediated, and states the residual/accepted risks and the threat model so the audit does not have to be re-derived from scratch each time.
YourMemory runs as a local, single-user, loopback-bound service by default:
- The FastAPI HTTP service binds
127.0.0.1:3033. The MCP/SSE server bindsYOURMEMORY_HOST(default127.0.0.1). - The HTTP API is intentionally unauthenticated on loopback. The trust boundary
is the local machine: any process running as the user already has equivalent
access to the DuckDB file at
~/.yourmemory/. - Cross-origin browser attacks (CSRF/DNS-rebinding) against the loopback API are
mitigated because every write endpoint requires
Content-Type: application/json, which forces a CORS preflight the server never approves (noAccess-Controlheaders are emitted by the FastAPI app). - The Cloudflare Worker (
yourmemory-backend) is the only internet-facing component. It handles email/OTP activation and an anonymous install counter.
The single most important operational control: never bind the service to a
non-loopback address (0.0.0.0) without putting an authenticating reverse proxy
in front of it. Doing so exposes all memory CRUD, /buffer, /observe,
/retrieve, and /agents/register to the network with no authentication. This is
by design for the local use case; it is documented here as the boundary condition.
| TSC | Area | Status | Notes |
|---|---|---|---|
| CC6.1 | Logical access — authentication | Partial (by design) | Local API is unauthenticated on loopback (see threat model). Agent API keys (ym_…) gate the private visibility partition and are SHA-256 hashed at rest, never stored in plaintext, shown once. |
| CC6.1 | Authorization — IDOR protection | Pass | All memory mutate paths (HTTP DELETE/PUT, MCP update_memory) enforce user_id ownership. Graph traversal filters by user_id. |
| CC6.6 | Boundary protection | Pass (default) | Loopback binding default; SSE host configurable and defaults to 127.0.0.1. |
| CC6.7 | Data in transit | Pass | Worker is HTTPS-only (Cloudflare edge). Activation token sent in POST body, not URL. |
| CC6.8 | Malicious code / injection | Pass | All SQL parameterized. No eval/exec/os.system on untrusted input. XSS sinks escaped. |
| CC7.1 | Vulnerability detection | Pass | Repeat security reviews logged in §3. Pickle/SSRF/injection vectors analyzed and cleared. |
| CC7.2 | Secrets management | Pass | No hardcoded secrets in source. Worker secrets (EMAILS_SECRET, RESEND_API_KEY, ANTHROPIC_API_KEY) are Cloudflare env bindings. API keys hashed. |
| A1.2 | Availability — fail-open design | Pass (intentional) | LLM judge, graph indexing, and recall fail open so a downed dependency never blocks the user. Not a confidentiality risk. |
| C1.1 | Confidentiality — user isolation | Pass | Cross-user leakage paths (cold-start, graph BFS, recall) all filter by user_id. |
- Stored XSS —
src/routes/ui.py: agent_id rendered via inlineonclick. → Replaced withdata-tab-id+addEventListener; all dynamic values run throughescHtml(). - IDOR — DELETE
/memories/{id}: no ownership check. →WHERE id = ? AND user_id = ?. - IDOR — PUT
/memories/{id}: no ownership check. → AddeduserIdquery param + 403 onowner_id != userId. - Auth bypass —
memory_mcp._check_registration:_token_verifiedset before validity confirmed. → Moved inside thedata.get("valid")branch. - Network exposure — MCP/SSE bound
0.0.0.0. →host = os.getenv("YOURMEMORY_HOST", "127.0.0.1").
- Auth bypass (regression risk) — PUT
/memories: the 403HTTPExceptionwas swallowed byexcept Exception: pass, falling through to the un-scoped UPDATE. → Addedexcept HTTPException: raiseahead of the generic handler. - Stored XSS —
src/routes/graph_viz.py: memorylabel/categoryinjected raw intoinnerHTML. →escHtml()on all values; error path usestextContent. - Cross-user exposure —
/graph/dataBFS + SQL had nouser_idfilter. →AND user_id = ?and post-fetch pruning ofnodes_to_include; returns empty if the seed node is not owned by the requester. - Stored XSS — Worker
/emails:email/instance_id/created_atraw in HTML. →escHtml()helper applied to all three. - Token in URL —
GET /verify-token?token=…leaked the activation token to logs/Referer/history. → Changed toPOST /verify-tokenwith token in JSON body;memory_mcp.pyclient updated to match.
- Auth bypass —
memory_mcp._check_registrationstill used the oldGET ?token=form, which now hits the worker's HTML catch-all, throws onjson.loads, is swallowed, and silently passes verification forever. → Updated to POST with JSON body. - IDOR — MCP
update_memorytool had no caller ownership check. → Addeduser_idto the tool schema and auser_id_owner != caller403-equivalent. - CSS-class injection —
graph_viz.py:categoryused as a CSS class name after HTML-escaping (which permits spaces). →safeCat()allowlist (fact|assumption|failure|strategy).
- OTP brute-force — Worker
/verify-otphad no per-code attempt cap. A 6-digit code (1e6 space) could be brute-forced inside the 10-minute window since only/send-otpwas rate-limited. → Added anattemptscolumn; the most-recent code is burned after 5 wrong guesses (HTTP 429).
| ID | Risk | Severity | Decision |
|---|---|---|---|
| R1 | Local HTTP API unauthenticated on loopback | Low (local trust boundary) | Accepted — matches single-user design. Mitigation: keep bound to 127.0.0.1. |
| R2 | /agents/register & /agents/revoke unauthenticated |
Low (localhost-only) | Accepted — only meaningful if bound to a non-loopback host. API keys gate only the private partition. |
| R3 | Worker /emails & /debug admin auth via ?token= query param compared with !== |
Low | Accepted — single-admin, Cloudflare edge jitter defeats practical timing attacks; secret is a long random value. Future hardening: move to Authorization header + constant-time compare. |
| R4 | pickle.load on ~/.yourmemory/graph.pkl |
None (not network-reachable) | Cleared — file is local-only; exploiting it requires pre-existing local write access. |
- SQL injection — every query uses
?/%splaceholders. The one f-string (graph_vizIN-clause) interpolates onlyints from graph traversal; user scope values inretrieve._scope_fragmentare bound, not interpolated. - SSRF — outbound hosts (
OLLAMA_URL, OpenAI/Anthropic bases) come from env vars / hardcoded constants, never from request input. - Command injection / RCE — no
os.system,subprocesswith shell, orevalon untrusted input anywhere in the request path. - CSRF/DNS-rebinding on loopback — blocked by the JSON content-type preflight requirement (server emits no CORS allow headers).
- Secrets in source — none. Confirmed across Python and the worker.
- Re-read every file in §Scope; diff against this document's §3 to confirm fixes
are still in place (grep anchors:
except HTTPException,YOURMEMORY_HOST,You do not own this memory,safeCat,POST /verify-token,MAX_OTP_ATTEMPTS). - Search for new sinks:
innerHTML, f-string SQL,pickle.load,subprocess,eval,os.system, new unauthenticated routes. - Append any new findings to §3 with the version that fixed them; update §4 if the risk acceptance changes.
Reporting a vulnerability: email mishrasachit1@gmail.com.