You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.)
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.
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.
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:
/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.
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:
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:
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.
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.
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.)
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.
Activity
H4CKS4F3 commentedon May 7, 2026
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.
H4CKS4F3 commentedon May 15, 2026
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 terminalSummary
Any local process that can reach
127.0.0.1:34567- including any Chrome extension installed in the user's browser withhttp://127.0.0.1:*in itshost_permissions- can drive aclaudePTY as the user. The browser doesn't enter the repro;curlmakes the same requests the malicious extension would.The root cause is two collaborating issues:
/healthreturnsAUTH_TOKENto any caller whoseOriginheader starts withchrome-extension://. The check isstartsWith, so any string after the prefix passes./wsupgrade accepts anychrome-extension://*Origin becauseBROWSE_EXTENSION_IDis not set by the production connect handler incli.ts.Setup
On a developer machine, start the daemon in headed mode:
$B connectConfirm it is running on the well-known port
34567:curl -s http://127.0.0.1:34567/health | head -c 200The response does not include a
tokenfield - noOriginheader was sent.Step 1 - Leak the root token
Add
Origin: chrome-extension://anything. Any 32-char string works because the server usesstartsWith:curl -s \ -H "Origin: chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ http://127.0.0.1:34567/healthThe
tokenfield is now present in the JSON response. Capture it:Step 2 - Mint a PTY session token
The server has now added
PTY_TOKENto the terminal-agent's in-memoryvalidTokensset via the loopback/internal/granthandshake.Step 3 - Open the PTY WebSocket
Install
websocatif you don't have it (brew install websocatorcargo install websocat), then: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:
You should see a
claudeprocess 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 feedclaudearbitrary input - including invocations of its Bash tool - and read all output.Expected behavior after fix
After the recommended fix is applied (stop returning
AUTH_TOKENvia/healthand requireBROWSE_EXTENSION_IDto be set):tokenfield, regardless ofOrigin.401 Unauthorizedbecause there is no longer a way for an external caller to obtain the bearer.403 forbidden originif the WebSocket Origin does not match the configuredBROWSE_EXTENSION_ID.Acknowledgment in source
The team is already aware of this issue. From line 7 (permalinked) browse/src/pty-session-cookie.ts:
The fix has been deferred. This finding is the case for not deferring it further.
josuediazflores commentedon Jun 9, 2026
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 (themakeFetchHandlerroute dispatcher inbrowse/src/server.tschecks neitherHostnorOriginon the routes themselves). On top of the headed-mode/healthtoken return documented here, that leaves a DNS-rebinding vector: a malicious web page the user visits can rebind an attacker-controlled hostname to127.0.0.1, at which point page JS is "same-origin" to the rebound host and can read the/healthresponse — 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_IDfixes 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.)
v1.91.19.0 fix: severe fix wave (truthful gates, no lost memory, ever…
capy-ai commentedon Oct 4, 2026
The sidebar terminal's
/wsendpoint now rejects every Origin except the pinned gstack extension (forks set theirs withgstack-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-upgradein Claude Code, then start a new session.If you still see this, please reopen with
./setup --statusoutput.v1.91.19.0 fix: severe fix wave (truthful gates, no lost memory, ever…