Fix guide · high · auth_token_in_websocket_url
Auth token embedded in WebSocket connection URL
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:
- Cloudflare, AWS ALB, GCP load balancers, nginx, and HAProxy all log the WS upgrade request URL by default. The token is in the access log on every box the connection touches.
- Browser DevTools shows the full URL in the Network tab — a screenshot of "the connection isn't working" can leak the token to anyone the developer asks for help.
- Browser history doesn't store WebSocket URLs the way it stores HTTP, but extensions that inspect network traffic (and there are many) capture them.
- WS connections often live in shared infrastructure (gateways, message brokers) where the URL is the only identifying field; ops teams casually paste full URLs into chat.
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:
- Sub-protocol-based auth — the
Sec-WebSocket-Protocolheader IS settable from the browser (it's the secondnew WebSocket(url, protocols)arg). Servers can accept a token via subprotocol. - Cookie-based auth — if the WS endpoint is same-origin (or CORS-compatible), the browser attaches your session cookie automatically.
- First-message auth — connect anonymously, send a
{ type: "auth", token }message as the first frame, server kicks the connection if invalid. - 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