-
Notifications
You must be signed in to change notification settings - Fork 5
fix: send terminal auth via first WebSocket message instead of URL query param #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7592ac
1bfd1e0
fab289c
0af6d2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |||||||||||
| interface WebSocketData { | ||||||||||||
| type: 'terminal'; | ||||||||||||
| workspaceName: string; | ||||||||||||
| authenticated: boolean; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| function createAgentServer( | ||||||||||||
|
|
@@ -89,6 +90,7 @@ | |||||||||||
| isWorkspaceRunning, | ||||||||||||
| isHostAccessAllowed: () => currentConfig.allowHostAccess === true, | ||||||||||||
| getPreferredShell, | ||||||||||||
| getAuthToken: () => currentConfig.auth?.token, | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| const triggerAutoSync = () => { | ||||||||||||
|
|
@@ -143,32 +145,34 @@ | |||||||||||
| return staticResponse; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const authResult = checkAuth(req, currentConfig); | ||||||||||||
| if (!authResult.ok) { | ||||||||||||
| return unauthorizedResponse(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const terminalMatch = pathname.match(/^\/rpc\/terminal\/([^/]+)$/); | ||||||||||||
|
|
||||||||||||
| if (terminalMatch) { | ||||||||||||
| const type: WebSocketData['type'] = 'terminal'; | ||||||||||||
| const workspaceName = decodeURIComponent(terminalMatch[1]); | ||||||||||||
|
|
||||||||||||
| const authResult = checkAuth(req, currentConfig); | ||||||||||||
|
|
||||||||||||
| const running = await isWorkspaceRunning(workspaceName); | ||||||||||||
| if (!running) { | ||||||||||||
| return new Response('Not Found', { status: 404 }); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const upgraded = server.upgrade(req, { | ||||||||||||
| data: { type, workspaceName }, | ||||||||||||
| data: { type, workspaceName, authenticated: authResult.ok }, | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| if (upgraded) { | ||||||||||||
| return undefined; | ||||||||||||
| } | ||||||||||||
| return new Response('WebSocket upgrade failed', { status: 400 }); | ||||||||||||
|
Check failure on line 168 in src/agent/run.ts
|
||||||||||||
|
Comment on lines
154
to
168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Identified by Warden via
Comment on lines
148
to
168
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
gricha marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const authResult = checkAuth(req, currentConfig); | ||||||||||||
| if (!authResult.ok) { | ||||||||||||
| return unauthorizedResponse(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if (pathname === '/health' && method === 'GET') { | ||||||||||||
| const identity = getTailscaleIdentity(req); | ||||||||||||
| const response: Record<string, unknown> = { status: 'ok', version: pkg.version }; | ||||||||||||
|
|
@@ -198,9 +202,9 @@ | |||||||||||
|
|
||||||||||||
| websocket: { | ||||||||||||
| open(ws: ServerWebSocket<WebSocketData>) { | ||||||||||||
| const { type, workspaceName } = ws.data; | ||||||||||||
| const { type, workspaceName, authenticated } = ws.data; | ||||||||||||
| if (type === 'terminal') { | ||||||||||||
| terminalHandler.handleOpen(ws, workspaceName); | ||||||||||||
| terminalHandler.handleOpen(ws, workspaceName, authenticated); | ||||||||||||
| } | ||||||||||||
| }, | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
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.
Identified by Warden via
security-review· medium, high confidence