A bearer token is a key that opens any door it touches, so the moment it rides along inside a request it can quietly walk out the front door to a stranger's server.
An OAuth access token (a "bearer" token) is a credential: whoever holds it can act as the user. So the whole game is keeping it in places only your app can see.
The classic mistake is putting the token in the page URL, or letting it sit in a request URL. When that page loads anything from a third party — an analytics script, an image on a content delivery network (CDN), a font — the browser politely attaches a Referer header naming the page that triggered the request. That header carries the full URL, token included, straight to an origin you don't control. The token didn't get hacked out; it was handed over.
The fix is to keep the token out of any place the browser will copy elsewhere. Send it in the Authorization header (never the URL), keep it short-lived, and tell the browser not to broadcast page URLs cross-origin with a Referrer-Policy. With no-referrer the header is dropped entirely; strict-origin-when-cross-origin (a sensible default) sends only the bare origin, never the path or query.
# Server response headers on every page of the app
# 1. Stop the browser leaking full URLs to other origins.
Referrer-Policy: strict-origin-when-cross-origin
# 2. Defence in depth: limit where the page may even load from.
Content-Security-Policy: default-src 'self'; img-src 'self' https://cdn-metrics.io
# Calling the API: token rides in the header, never the URL.
GET /v1/me HTTP/1.1
Host: api.example
Authorization: Bearer tok_a3f9c2e7
# bad: GET /v1/me?access_token=tok_a3f9c2e7 <-- token in URL, leaks via Referer
When something does slip, the response is mechanical: revoke and rotate the exposed token so the copy in someone else's logs is dead, then close the path that leaked it.
| Where you see it | What it implies |
|---|---|
Third party's access logs show your token in a Referer value | A page holding the token loaded a cross-origin resource; the token is now in a system you don't control |
| Token appears in a URL path or query in your own analytics or CDN logs | The token was embedded in a URL — the root cause; anything that ingests URLs has copied it |
| API calls from an IP range or user-agent that never belonged to that user | Possible replay: someone is using the leaked bearer token directly |
Browser dev tools or a proxy capture shows ?access_token= on requests | The token rides in the URL and will be sent in Referer on the next sub-resource fetch |
| Error trackers or support screenshots containing a full app URL | Tokens in URLs leak into bug reports and pasted links, not just network headers |
Referer header. The Authorization header is the only safe place for a bearer token.Referrer-Policy on some routes only. The OAuth redirect and callback page is exactly where tokens and codes appear; apply the policy site-wide, not per-page.A dashboard at app.example renders the user's access token into the page URL so a deep link can be shared: /dash?access_token=tok_a3f9c2e7. The same page embeds an analytics pixel from cdn-metrics.io. When the browser fetches that pixel, it sends Referer: https://app.example/dash?access_token=tok_a3f9c2e7 — the live token, in plain text, into a third party's logs. Days later, the metrics vendor's log export is reviewed and the token is spotted. Triage confirms the value is a real, still-valid token. Containment: the token is revoked and the user's session rotated, so the copy in the vendor's logs is now useless; then Referrer-Policy: strict-origin-when-cross-origin is shipped so only https://app.example would have been sent. Root cause: the token never belonged in the URL — it moves to the Authorization header, and deep links use a short opaque id instead.
1. Your app puts the access token in the page URL and loads an image from a third-party CDN. What is the most direct way the token reaches that CDN?
2. You confirm a live token leaked into a vendor's logs. What is the first containment step?