Skip to content

fix: send terminal auth via first WebSocket message instead of URL query param#170

Merged
gricha merged 4 commits intomainfrom
fix/terminal-websocket-auth
Feb 16, 2026
Merged

fix: send terminal auth via first WebSocket message instead of URL query param#170
gricha merged 4 commits intomainfrom
fix/terminal-websocket-auth

Conversation

@gricha
Copy link
Owner

@gricha gricha commented Feb 16, 2026

Summary

  • Browsers can't send Authorization headers on WebSocket upgrade requests, breaking terminal auth from web/mobile UIs
  • Tokens in URLs leak via server logs, proxy logs, browser history, and Referer headers — send auth as first WebSocket message instead
  • Server upgrades terminal WebSockets unconditionally, then expects { type: 'auth', token } as first message before allowing interaction
  • Connections that fail first-message auth are closed with code 4001
  • Bearer header and Tailscale auth still work at the HTTP level for upgrade requests
  • Updated all clients (web, mobile, CLI) and integration tests

Browsers can't send custom Authorization headers on WebSocket upgrade
requests, causing terminal connections from web/mobile UIs to fail auth.
Comment on lines 324 to 325
if (currentToken) {
url.searchParams.set('token', currentToken);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [AJ4-ZWP] Authentication token transmitted in cleartext over WebSocket (high confidence)

Auth token passed in URL query param over unencrypted ws:// connection. Tokens are visible in logs/history and sent in plaintext without TLS, allowing interception and account takeover.

Suggested fix: Use wss:// (WebSocket over TLS) instead of ws:// for encrypted transport. baseUrl should use https:// to ensure wss:// protocol. Additionally, consider passing tokens in the first WebSocket message after upgrade instead of URL params to avoid logging.

Suggested change
if (currentToken) {
url.searchParams.set('token', currentToken);
baseUrl = `https://${normalizeHost(host)}:${port}`;

Identified by Warden via security-review · high, high confidence

Comment on lines 188 to 190
if (this.token) {
url.searchParams.set('token', this.token);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [ZHD-3Y4] Authentication token exposed in WebSocket URL query parameters (high confidence)

Auth tokens in URLs can leak via browser history, access logs, proxy logs, and Referer headers. While this addresses WebSocket header limitations, consider session-scoped tokens or rotating short-lived tokens for WebSocket upgrades instead of passing long-lived auth tokens in URLs.

Suggested fix: Consider implementing a token exchange: use the main auth token to request a short-lived WebSocket-specific token from an HTTP endpoint, then pass that ephemeral token in the URL. This limits exposure if URLs are logged or leaked.

Suggested change
if (this.token) {
url.searchParams.set('token', this.token);
}
// TODO: Replace with short-lived WebSocket token from token exchange endpoint
// to minimize exposure if URL is logged or leaked

Identified by Warden via security-review · high, high confidence

Comment on lines 219 to 222
const url = new URL(`${wsProtocol}${host}/rpc/terminal/${encodeURIComponent(workspaceName)}`);
if (token) {
url.searchParams.set('token', token);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [94W-JJJ] Authentication token exposed in WebSocket URL query parameters (high confidence)

Authentication tokens passed via URL query parameters (?token=...) are exposed in browser history, server logs, and referrer headers. While necessary for WebSocket auth (browsers can't set custom headers on upgrade), this creates token leakage risk.

Suggested fix: The implementation is correct given WebSocket limitations. To mitigate risks: 1) Use short-lived tokens for terminal connections, 2) Ensure HTTPS/WSS in production to prevent network sniffing, 3) Consider token rotation after each terminal session. Document this security consideration.

Suggested change
const url = new URL(`${wsProtocol}${host}/rpc/terminal/${encodeURIComponent(workspaceName)}`);
if (token) {
url.searchParams.set('token', token);
}
/**
* Security note: Token is passed as query parameter due to WebSocket upgrade limitations.
* Ensure WSS (not WS) is used in production to prevent token interception.
*/

Identified by Warden via security-review · medium, high confidence

…URL query param

Tokens in URLs leak via server logs, proxy logs, browser history, and
Referer headers. Move to first-message auth: the server upgrades the
WebSocket unconditionally, then expects { type: 'auth', token } as the
first message before allowing any terminal interaction. Connections that
fail auth are closed with code 4001.

Bearer header and Tailscale auth still work at the HTTP level for
upgrade requests that already carry credentials.
Comment on lines 154 to 168
@@ -169,6 +168,11 @@ function createAgentServer(
return new Response('WebSocket upgrade failed', { status: 400 });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [4ER-FDL] WebSocket upgrade allowed without authentication (high confidence)

The terminal WebSocket endpoint allows upgrade before authentication. While deferred auth happens in the handler, this creates a window where unauthenticated WebSocket connections exist. The connection can be upgraded (line 161) even when authResult.ok is false, allowing unauthenticated clients to establish WebSocket connections that are only closed later in the handler.

Suggested fix: Reject WebSocket upgrade if authentication fails, or ensure auth check happens before upgrade

Suggested change
if (!authResult.ok) {
return unauthorizedResponse();
}
data: { type, workspaceName, authenticated: true },

Identified by Warden via security-review · high, high confidence

Comment on lines +103 to +105
if (token) {
ws.send(JSON.stringify({ type: 'auth', token: token }));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [UZX-4PN] Authentication token sent over unencrypted WebSocket connection (high confidence)

The auth token is sent via WebSocket message without enforcing TLS/WSS. If users connect via ws:// instead of wss://, the token is transmitted in cleartext and can be intercepted by network attackers.

Suggested fix: While this file cannot directly enforce WSS, document that this auth method requires WSS in production. The enforcement should happen at connection time or in server configuration.

Suggested change
if (token) {
ws.send(JSON.stringify({ type: 'auth', token: token }));
}
// SECURITY: Token should only be sent over WSS (encrypted) in production

Identified by Warden via security-review · medium, high confidence

Comment on lines 148 to 168
@@ -169,6 +168,11 @@ function createAgentServer(
return new Response('WebSocket upgrade failed', { status: 400 });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [K44-7DE] Authentication mismatch with PR description (high confidence)

PR description states tokens are passed via query param (?token=), but the actual implementation uses WebSocket message-based authentication (auth.ts shows only Authorization header support, terminal handler expects auth messages). This inconsistency suggests either incomplete implementation or misleading documentation. If query params were intended but not implemented, tokens could be logged in server access logs, proxy logs, and browser history.

Identified by Warden via security-review · medium, high confidence

@gricha gricha changed the title fix: terminal WebSocket auth via query param fix: send terminal auth via first WebSocket message instead of URL query param Feb 16, 2026
@gricha gricha marked this pull request as ready for review February 16, 2026 23:02
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [9MH-LFT] WebSocket authentication lacks rate limiting (high confidence)

Authentication attempts via WebSocket message have no rate limiting or account lockout. Attackers can repeatedly attempt authentication without throttling, enabling brute force attacks against the auth token.

Identified by Warden via security-review

@gricha gricha merged commit 1b81d9f into main Feb 16, 2026
16 checks passed
@gricha gricha deleted the fix/terminal-websocket-auth branch February 16, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments