Security: XSS, no-auth CORS, and persistent prompt injection via stored memories
During a security review of locally installed developer tools, I found several vulnerabilities in opencode-mem's web server component. Line numbers refer to the compiled dist/ output (v installed via npm opencode-mem@latest); please map to src/ equivalents.
1. XSS via unescaped memoryType — MEDIUM
Files: dist/web/app.js:186 and dist/web/app.js:284
The memory.memoryType field is interpolated directly into HTML without escapeHtml(), unlike other user-controlled fields (prompt.content, memory.displayName, memory.tags) which are properly escaped.
${memory.memoryType ? `<span class="badge badge-type">${memory.memoryType}</span>` : ""}
memoryType is user-controlled via:
- The agent's
memory tool add mode (args.type in dist/index.js:272)
- The web API POST
/api/memories (data.type in dist/services/api-handlers.js:253)
Impact: Storing a memory with type containing <img src=x onerror=alert(1)> executes JavaScript when viewed in the web UI.
Fix: Apply escapeHtml(memory.memoryType) like other fields.
2. Web server has no authentication and permissive CORS — MEDIUM
File: dist/services/web-server.js:340-349
The web server (default 127.0.0.1:4747) has no authentication on any endpoint. Combined with Access-Control-Allow-Origin: *, any website the user visits can make cross-origin requests to read, add, modify, or delete the user's memories.
jsonResponse(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
While Content-Type: application/json triggers CORS preflight, an attacker can use Content-Type: text/plain (a "simple" request that bypasses preflight) — Bun's req.json() parses the body as JSON regardless of Content-Type.
If the user configures webServerHost: "0.0.0.0", any network host can access the API.
Impact: Cross-origin data theft/modification of all stored memories (which may contain sensitive project information and conversation summaries).
Fix: Bind CORS to 127.0.0.1/localhost origins only, or require a token. Consider adding authentication.
3. Persistent prompt injection via stored memories — MEDIUM
Files: dist/index.js:159-190 (chat.message hook), dist/services/context.js:3-24
The chat.message hook injects stored memories into chat context as synthetic text parts without any sanitization against prompt injection:
// dist/services/context.js:14-18
parts.push(`- [${similarity}%] ${content}`);
Memory content can be influenced by prompt injection: if the agent fetches a malicious website via web_fetch, the website content could instruct the agent to use the memory tool's add mode to store arbitrary instructions (e.g., "Ignore all previous instructions..."). This stored content is then injected into future chat sessions, creating a persistent prompt injection vector that survives across sessions.
The stripPrivateContent function (dist/services/privacy.js) only redacts <private> tags — it does not sanitize against prompt injection markers.
Impact: A malicious website can plant persistent instructions in the user's memory store that activate in future sessions.
Fix: Consider sandboxing injected memory content (e.g., fence as data, add system-level instruction to treat memory content as untrusted data not instructions).
4. Unescaped memory.id in web UI — LOW
Files: dist/web/app.js:203 and dist/web/app.js:305
<span>ID: ${memory.id}</span>
memory.id is not escaped with escapeHtml(). The ID is system-generated (mem_${Date.now()}_${Math.random()...}) so low risk in normal operation, but if an attacker directly inserts into the SQLite database (possible due to the lack of authentication on the web API), it could trigger XSS.
Fix: Apply escapeHtml(memory.id).
Workaround
Users can disable the web server entirely via config:
["opencode-mem", { "webServerEnabled": false }]
This closes the remote attack surface for issues #1, #2, and #3 (the web API write path). Core memory storage/retrieval still works in-process via SQLite.
Security: XSS, no-auth CORS, and persistent prompt injection via stored memories
During a security review of locally installed developer tools, I found several vulnerabilities in opencode-mem's web server component. Line numbers refer to the compiled
dist/output (v installed via npmopencode-mem@latest); please map tosrc/equivalents.1. XSS via unescaped
memoryType— MEDIUMFiles:
dist/web/app.js:186anddist/web/app.js:284The
memory.memoryTypefield is interpolated directly into HTML withoutescapeHtml(), unlike other user-controlled fields (prompt.content,memory.displayName,memory.tags) which are properly escaped.memoryTypeis user-controlled via:memorytooladdmode (args.typeindist/index.js:272)/api/memories(data.typeindist/services/api-handlers.js:253)Impact: Storing a memory with
typecontaining<img src=x onerror=alert(1)>executes JavaScript when viewed in the web UI.Fix: Apply
escapeHtml(memory.memoryType)like other fields.2. Web server has no authentication and permissive CORS — MEDIUM
File:
dist/services/web-server.js:340-349The web server (default
127.0.0.1:4747) has no authentication on any endpoint. Combined withAccess-Control-Allow-Origin: *, any website the user visits can make cross-origin requests to read, add, modify, or delete the user's memories.While
Content-Type: application/jsontriggers CORS preflight, an attacker can useContent-Type: text/plain(a "simple" request that bypasses preflight) — Bun'sreq.json()parses the body as JSON regardless of Content-Type.If the user configures
webServerHost: "0.0.0.0", any network host can access the API.Impact: Cross-origin data theft/modification of all stored memories (which may contain sensitive project information and conversation summaries).
Fix: Bind CORS to
127.0.0.1/localhostorigins only, or require a token. Consider adding authentication.3. Persistent prompt injection via stored memories — MEDIUM
Files:
dist/index.js:159-190(chat.message hook),dist/services/context.js:3-24The
chat.messagehook injects stored memories into chat context as synthetic text parts without any sanitization against prompt injection:Memory content can be influenced by prompt injection: if the agent fetches a malicious website via
web_fetch, the website content could instruct the agent to use thememorytool'saddmode to store arbitrary instructions (e.g., "Ignore all previous instructions..."). This stored content is then injected into future chat sessions, creating a persistent prompt injection vector that survives across sessions.The
stripPrivateContentfunction (dist/services/privacy.js) only redacts<private>tags — it does not sanitize against prompt injection markers.Impact: A malicious website can plant persistent instructions in the user's memory store that activate in future sessions.
Fix: Consider sandboxing injected memory content (e.g., fence as data, add system-level instruction to treat memory content as untrusted data not instructions).
4. Unescaped
memory.idin web UI — LOWFiles:
dist/web/app.js:203anddist/web/app.js:305memory.idis not escaped withescapeHtml(). The ID is system-generated (mem_${Date.now()}_${Math.random()...}) so low risk in normal operation, but if an attacker directly inserts into the SQLite database (possible due to the lack of authentication on the web API), it could trigger XSS.Fix: Apply
escapeHtml(memory.id).Workaround
Users can disable the web server entirely via config:
This closes the remote attack surface for issues #1, #2, and #3 (the web API write path). Core memory storage/retrieval still works in-process via SQLite.