Fix guide · high · auth_token_in_websocket_url

Auth token embedded in WebSocket connection URL

What this rule means

A WebSocket URL in your bundle includes an auth-shaped credential in its query string. Proxies and load balancers log the full WS connection URL the same way they log HTTP URLs — the token leaks to every system in the request path.

Why it matters

WebSocket connection URLs (wss://api.example.com/ws?access_token=...) hit the same logging surface as HTTP URLs:

The reason this pattern keeps appearing: the WebSocket API in browsers doesn't let you set custom headers. You can't say new WebSocket(url, { headers: { Authorization: 'Bearer ...' } }). So when the server requires per-connection auth, developers reach for the URL parameter as the only place to put it.

But there are alternatives, and they all work:

  1. Sub-protocol-based auth — the Sec-WebSocket-Protocol header IS settable from the browser (it's the second new WebSocket(url, protocols) arg). Servers can accept a token via subprotocol.
  2. Cookie-based auth — if the WS endpoint is same-origin (or CORS-compatible), the browser attaches your session cookie automatically.
  3. First-message auth — connect anonymously, send a { type: "auth", token } message as the first frame, server kicks the connection if invalid.
  4. Short-lived token — fetch a single-use connection token from your backend (POST /api/ws-ticket), use it in the URL, server invalidates after first use. Even if the URL leaks, the token is dead.

How to fix it

Preferred — first-message auth:

const ws = new WebSocket('wss://api.example.com/ws');
ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'auth', token: getToken() }));
});

The server holds the connection in an unauthenticated state until it receives a valid auth frame, then promotes the connection or kicks it.

Sub-protocol auth:

const ws = new WebSocket('wss://api.example.com/ws', ['bearer.' + getToken()]);

The server reads Sec-WebSocket-Protocol from the upgrade request, validates, and echoes one of the offered protocols on accept.

Single-use ticket:

const ticket = await fetch('/api/ws-ticket', { credentials: 'include' }).then(r => r.text());
const ws = new WebSocket(`wss://api.example.com/ws?ticket=${ticket}`);

Server issues short-lived (~10 second) single-use tickets, invalidates each on connect. The ticket might leak to logs but is useless to a later attacker.

Audit logs after rotation. If the existing token is long-lived, treat it as compromised — anyone who scraped your proxy logs in the last N days has it. Rotate, force re-auth.

Did vibecheck flag this on your app?

If you reached this page from a vibecheck inspection report, the redacted match in your scan output is the exact string we found in your bundle. After applying the fix above, run the inspection again — the finding should clear.

Run another inspection