OAuth token leak

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.

The idea

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.

See it work

your browser app session app.example trusted origin cdn-metrics.io third-party origin tok_a3f…
A user is signed in. Press play to watch the token leak, then get contained.

How it works

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.

Signals

Where you see itWhat it implies
Third party's access logs show your token in a Referer valueA 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 logsThe 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 userPossible replay: someone is using the leaked bearer token directly
Browser dev tools or a proxy capture shows ?access_token= on requestsThe 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 URLTokens in URLs leak into bug reports and pasted links, not just network headers

Watch out for

Worked example

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.

Check yourself

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?