A user clicks a link out of your application. Two things go with them that you probably did not intend: the address of the page they were on, and in some cases a handle that lets the destination navigate your tab.
Both are defaults. Both are one attribute away from being fixed. And both matter more in an application than on a marketing site, because your URLs contain identifiers and sometimes capabilities.
This post is what leaves when someone clicks. For whoever renders links, including links in user-generated content.
The referrer sends your URL to someone else
By default, navigating to another site sends the address of the page you came from. On a content site that is harmless and useful. In an application it can include:
- Record and tenant identifiers, revealing your internal structure and your customers' data volumes.
- A token in a URL: a share link, a presigned URL, a reset or invitation token. Anything in the query string or the path is in the referrer.
- Parameters describing what the user was doing.
The fix is a referrer policy. strict-origin-when-cross-origin is a reasonable default for most applications: full referrer within your own origin, only the origin when leaving, nothing when downgrading to HTTP.
Referrer-Policy: strict-origin-when-cross-origin
For pages whose URL is itself sensitive, go further and send nothing:
<meta name="referrer" content="no-referrer">
Set it as a header globally, and override per link where you need to. The header is the control; per-element attributes are for exceptions.
Third-party resources leak it too
Worth knowing because it is less obvious: the referrer is sent on subresource requests as well as navigations. An image, a script or a font loaded from another origin receives the address of the page loading it.
So a page with a token in its URL that loads an analytics script has already sent that URL to the analytics vendor, before the user clicked anything. This is the same mechanism as the link-unfurling problem, arriving from a different direction, and it is why keeping capabilities out of URLs matters more than any policy setting.
target="_blank" hands over a window handle
A link opening in a new tab gives the opened page a reference back to yours, unless you say otherwise. That reference allows the destination to navigate your tab to a different address.
The attack is straightforward: a user clicks a link in your application, the new tab shows something innocuous, and it quietly replaces the original tab with a convincing copy of your login page. The user finishes reading, returns to the first tab, and signs in again.
Modern browsers default to implicit noopener for target="_blank", which closes this in current versions. It is still worth being explicit, because older browsers, embedded webviews and non-browser clients do not all behave the same way, and being explicit costs nothing:
<a href="https://external.example" target="_blank" rel="noopener noreferrer">
Links in user-generated content are the priority
Everything above matters most where the destination is chosen by someone else: a link in a comment, a profile field, a support ticket, a document, a webhook description.
Three rules for rendering those:
Allowlist the scheme. Permit http and https and nothing else. javascript: is script execution, and data: can be a document. This is the most important of the three.
Always add rel="noopener noreferrer", without exception, because you do not control the destination.
Consider an interstitial for links leaving your application in contexts where users are likely to trust them, which tells the user they are leaving and where to. This is friction and it is warranted where an attacker can place links in front of other customers.
Check yours
# Is a referrer policy set?
curl -sI https://app.example.com/ | grep -i referrer-policy
# Do external links carry rel attributes?
curl -s https://app.example.com/some-page-with-links \
| grep -oE '<a [^>]*target="_blank"[^>]*>' | grep -v noopener | head
# Does user content allow dangerous schemes?
# Submit a link with javascript: and data: through your own product,
# then read the rendered HTML.
The third one is the test that finds real problems, and it has to be done through the product rather than against the sanitiser in isolation, because the encoding path between input and render is where the gap usually is.
The concession
A restrictive referrer policy breaks things people rely on. Analytics attribution degrades, some third-party integrations use the referrer to verify where a request came from, and affiliate or partner arrangements sometimes depend on it.
So the honest sequence is to set a sensible global default, find what breaks, and grant per-destination exceptions deliberately rather than loosening the default. The marketing pages that need attribution are usually not the pages whose URLs carry tokens, which makes the split natural.
The implication
Your URLs are more sensitive than they look, and the browser shares them by default with every destination a user visits and every third party a page loads.
One header and one rel attribute cover most of it. The part that needs actual thought is not leaving capabilities in URLs in the first place, because once they are there, every link and every script tag is a way out.