When a WebSocket client puts its access token in the wss: URL query string, where does that token land and what replaces the practice?
answer
- the query string is the request target
- everything on the path logs request targets
- TLS protects flight, not storage
- swap a long life for seconds and one use
- take-and-delete, bound to the minting subject
basics
~20 sIt lands in the server's access log, in every intermediary's access log, in browser history, and in every copy of that URL. Replace it with a short-lived single-use ticket minted on an ordinary authenticated request and redeemed on the upgrade.
solid answer
~40 sThe query string is part of the request target, and request targets are what everything on the path writes down: the server's own access log, every intermediary's log, browser history, and any bug report or debug link the URL is pasted into. TLS does not help — it protects the value in flight, and every one of those records is written after the connection is terminated and the request parsed. The replacement is a **ticket**: the client makes an ordinary authenticated request to a small endpoint, gets back an opaque value good for seconds and for one redemption, and puts *that* in the upgrade URL. The ticket still lands in all four places. The difference is that what is written down is already expired and already spent.
code
http · 16 linesPOST /api/socket-tickets HTTP/1.1
Host: console.example.org
Authorization: Bearer <access token>
Content-Length: 0
HTTP/1.1 200 OK
Content-Type: application/json
{"ticket":"kQ7mS1xbN0","expiresInSeconds":15}
GET /cabinet/socket?ticket=kQ7mS1xbN0 HTTP/1.1
Host: console.example.org
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13go deeper
Remember that the query string is part of the request line and that request lines are logged, so a credential placed there is a credential written to disk in several places.
Explain why encryption does not help — every logger on the path has already decrypted and parsed the request — and describe the mint-then-redeem sequence.
Argue the trade honestly: the ticket leaks identically and is worthless anyway, and defend the bounds — seconds, one atomic redemption, principal taken from the stored record.
Decide whether one extra round trip on every socket open is a price your product pays everywhere, or only where the credential is broadly scoped, and who owns the ticket store.
## Why the token ends up in the URL in the first place A page cannot set a field on the upgrade. It controls the URL and the subprotocol list, and the URL is the obvious one. So `wss://console.example.org/cabinet/socket?access_token=...` gets written, it works on the first try, and it ships. This is the single most common credential leak on this protocol, and it ships for the same reason every time: **nothing about it fails.** ## The four places it lands The query string is part of the **request target** — the first line of the request. Everything that logs requests logs request targets, by default: 1. **The server's own access log.** Method, target, status, bytes. The target includes the query string, so the token is now in a file with a retention policy written for traffic analysis, readable by anyone who can read logs. 2. **Every intermediary's access log.** A reverse proxy, an L7 load balancer or a CDN edge on the path terminates the connection, parses the request and logs the same line. You may not own those logs, and you may not know how long they are kept. 3. **Browser history.** The URL the page opened is a URL the client remembers. 4. **Every copy of the URL.** Crash reports, error pages, support tickets, a debug link pasted into a chat — a URL is a thing people share, and this one carries a credential. If the URL ever leaves the page in a link or a redirect, it can travel in a `Referer` request field as well. **TLS is not a defence here.** It protects the request in flight between hops. Every one of those four records is written by a party that has already decrypted and parsed the request — that is the whole job of an access log. A candidate who answers "but it is wss, so it is encrypted" has named the one protection that does not apply. ## The ticket, and the precise reason it is better The replacement is a **short-lived, single-use ticket**: 1. The client makes an ordinary authenticated request — an HTTP request where it *can* set `Authorization`, or one the session cookie authenticates — to a small endpoint. 2. The server mints an opaque value, records it against the authenticated subject with an expiry seconds away, and returns it. 3. The client opens the socket with that value in the query string. 4. The server **takes** the record — reads and deletes it atomically — checks the expiry, binds the resulting connection to the subject the record names, and completes the handshake. Be honest about what this does and does not change. **The ticket lands in exactly the same four places.** It is still a query-string value; the access logs still record it. What changes is the value of what was recorded: | Property | Long-lived token in the URL | Ticket in the URL | |---|---|---| | Appears in access logs | yes | yes | | Useful to whoever reads them | for as long as it is valid — often hours | no: expired within seconds | | Replayable | yes, repeatedly | no: redemption deletes it | | Scope if stolen | everything the token authorizes | one socket, as one subject | | Extra round trip | none | one authenticated request | That is the whole argument, and stating it this way is what separates a candidate who has read a checklist from one who has made the decision. ## The bounds a ticket needs A ticket that is not bounded is just a second token: - **Seconds of validity**, not minutes. It is redeemed immediately after it is minted; there is no legitimate reason for a long window. - **One redemption.** Enforce it by *removing* the record as you read it, not by marking it used afterwards — the gap between read and mark is a race. - **Bound to the subject that minted it.** The connection's principal comes from the stored record, never from anything the client presents on the upgrade. - **Opaque and unguessable.** It names nothing and carries no claims; it is a lookup key into server state. - **Not a substitute for the real credential.** The identity work happens on the minting request; the ticket only carries the result of it across one hop. ## What the interviewer is actually testing That you know a query string is a logged surface, that you do not reach for TLS as the answer, and that you can say what the ticket *does not* fix as clearly as what it does. The bonus point is noticing that a non-browser client should not be doing any of this — it can set a request field and skip the whole dance.
- Why not just make the token short-lived and keep it in the query string?That is most of the benefit, and for some systems it is enough. The two things it still lacks are single use — a short-lived token in a log is replayable for its whole window by anyone who reads that log quickly — and scope: the token authorizes everything it normally authorizes, not just one socket. A ticket gives up both, which is why it is the shape that survives review.
- Where should the redemption check live, and what must it be careful about?In the upgrade handler, before the handshake completes, so a bad ticket never becomes a socket. The care is in making redemption atomic: read and remove in one operation. Reading, validating, then marking the ticket used leaves a window where two concurrent upgrades both see a valid record and both succeed.
- Does any of this apply to a client that is not a browser?Not really. A service builds the upgrade request itself and can set `Authorization` on it, so it never needs a ticket. Making service clients mint tickets because the page ones do adds a round trip and a store for no gain — the ticket exists purely to work around a page's inability to set a request field.
saying these in an interview costs you the question
- Says wss encryption keeps the query-string token out of logs
- Claims a ticket in the URL does not appear in access logs
- Treats a short expiry as making a credential single-use
- Marks a ticket used after validating it, instead of taking it atomically
- Takes the connection's identity from a claim the client sends on the upgrade
- Assumes only your own logs record the request target