Files
Kordant/tasks/security-fixes/08-fix-websocket-jwt-query-param-leak.md
2026-05-29 09:03:47 -04:00

3.3 KiB

08. Fix WebSocket JWT leakage via query parameter

meta: id: security-fixes-08 feature: security-fixes priority: P1 depends_on: [] tags: [implementation, tests-required, medium-severity]

objective:

  • Move WebSocket JWT authentication from query parameter to Authorization header to prevent token leakage in logs

deliverables:

  • Updated getTokenFromRequest() at web/src/server/websocket.ts:39-43 to read from Authorization header
  • Updated WebSocket client code to send JWT in the Authorization header
  • Backward compatibility during transition period (optional)
  • Unit tests for token extraction from headers

steps:

  1. Examine web/src/server/websocket.ts:39-43 (getTokenFromRequest) and web/src/server/websocket.ts:56-67 (authentication flow)
  2. Update getTokenFromRequest() to:
    • First check the Authorization header for a Bearer token (Authorization: Bearer <jwt>)
    • Fall back to ?token= query parameter with a deprecation warning (optional, for backward compatibility)
    • Return null if neither is present
  3. Update the WebSocket client (browser app and extension) to:
    • Include the JWT in a custom header: new WebSocket(url, { headers: { Authorization: Bearer ${token} } })
    • Note: The WebSocket constructor doesn't support custom headers in browsers; use the verifyClient callback on the server side to read headers from the upgrade request
    • Alternative: Use an HTTP handshake approach or pass the token in a message after connection (with a timeout)
  4. If browser WebSocket API limitations prevent header-based auth, implement a post-connection authentication message with a timeout (e.g., client must send {type: "auth", token: "..."} within 5 seconds)
  5. Update web/src/server/websocket.ts:80-102 (connection handler) to enforce the new auth flow

tests:

  • Unit: getTokenFromRequest() extracts token from Authorization header
  • Unit: getTokenFromRequest() rejects connections without a token
  • Unit: Query parameter fallback (if implemented) logs a deprecation warning
  • Integration: WebSocket connection with valid Bearer token is authenticated
  • Integration: WebSocket connection without a token is rejected

acceptance_criteria:

  • JWT is no longer passed in the query parameter by default
  • Server accepts JWT from Authorization header (or post-connection auth message if browser limitation requires it)
  • Connections without authentication are rejected
  • No JWT tokens appear in server access logs or proxy logs
  • Backward compatibility is handled (if implemented) with a deprecation path

validation:

  • cd web && bun test — all tests pass
  • Connect via WebSocket with a valid token in the expected location and verify authentication succeeds
  • Connect without a token and verify the connection is rejected
  • Check server logs to confirm JWT tokens are not visible in URLs

notes:

  • Finding p8-008: The browser WebSocket API does not support custom headers in the constructor
  • Recommended approach: Post-connection authentication message with a server-side timeout
    1. Client connects without token
    2. Server allows the connection but marks it as unauthenticated
    3. Client sends {type: "auth", token: "..."} within 5 seconds
    4. Server validates the token and upgrades the connection
    5. Unauthenticated connections are closed after the timeout
  • This approach avoids JWT in URLs and works with the browser WebSocket API