Skip to content

Security: XSS, no-auth CORS, and persistent prompt injection via stored memories #135

Description

@blackjlc

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions