Skip to content

Brainstorm server WebSocket lacks Origin validation, enabling cross-origin prompt injection #1014

@benmosher

Description

@benmosher

Preamble

Hello! I asked Claude to review the latest main of this project, mostly for current data/secret exfiltration attempts.

The only thing it surfaced is the following websocket issue. It would be pretty tough to attack but at 125k+ stars, you've got a high-value target here 😅

Apologies if there is a reason it missed why this is not actually feasible in practice. I'm not up on all my CORS browser protections specifics, but this reads as theoretically plausible (albeit impractical) to me.

Summary

The brainstorm companion server (skills/brainstorming/scripts/server.cjs) accepts WebSocket connections from any origin without validating the Origin header. Combined with the random-but-discoverable port, a malicious webpage visited while a brainstorm session is active could connect to the server and inject arbitrary content into the AI's event stream.

Attack Scenario

  1. User starts a brainstorm session; server binds to ws://127.0.0.1:<random_port>
  2. User visits a malicious webpage (e.g. in another tab)
  3. The page scans localhost ports to find the brainstorm server (feasible via WebSocket connection timing)
  4. The page connects and sends a crafted event:
    {"type": "click", "choice": "x", "text": "<injected instructions>", "timestamp": 1}
  5. This is written verbatim to state_dir/events
  6. On the next turn, Claude reads the events file as trusted user feedback and may act on the injected content

The /files/ HTTP endpoint has the same exposure: once the port is found, any local page can read brainstorm files served from CONTENT_DIR.

Root Cause

handleUpgrade in server.cjs only checks for the presence of Sec-WebSocket-Key and does not inspect the Origin header:

function handleUpgrade(req, socket) {
  const key = req.headers['sec-websocket-key'];
  if (!key) { socket.destroy(); return; }
  // ← no Origin check

Fix

Validate the Origin header in handleUpgrade. Legitimate connections come from the browser navigating directly to http://localhost:<port> (Origin is null or matches the server's own host):

function handleUpgrade(req, socket) {
  const key = req.headers['sec-websocket-key'];
  if (!key) { socket.destroy(); return; }

  const origin = req.headers['origin'];
  const allowedOrigins = [
    \`http://localhost:\${PORT}\`,
    \`http://127.0.0.1:\${PORT}\`,
  ];
  if (origin && !allowedOrigins.includes(origin)) {
    socket.destroy();
    return;
  }
  // ...

For defense in depth, a random session token could also be added to the WebSocket URL (e.g. ws://localhost:<port>/<token>) so that even a page that guesses the port cannot connect without knowing the token.

Severity

Medium. Requires the user to visit a malicious page during an active brainstorm session, and exploitation depends on port scanning localhost — which most browsers allow for WebSocket connections. Impact is prompt injection into an active Claude session.

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