Every image CDN offers a domain allow-list: name the sites your images may be used from, and requests from anywhere else are refused. It is a useful feature. It is also, in almost every implementation including ours, not access control — and the difference matters the day somebody leaks a key.

Where the domain comes from

A request does not carry a trustworthy statement of who made it. What it carries is two headers:

  • Origin — the scheme, host and port of the page making the request.
  • Referer — the full URL of the page, subject to the site's referrer policy. Misspelled in the original specification, and we are all stuck with it.

Both are set by the client. In a browser that is a real guarantee: Origin and Referer are forbidden header names, so page JavaScript cannot change them. A page author genuinely cannot make their site claim to be yours.

Outside a browser there is no guarantee at all. One line of curl sets either header to anything:

curl -H "Referer: https://allowed.example.com/" https://cdn.example.com/v1/hero.jpg

The simpler bypass is to send nothing

Forging is not even necessary, because of a decision every one of these implementations has to make: what do you do with a request that declares no origin at all?

Refuse it, and you break legitimate traffic. Server-to-server callers send neither header — your own backend fetching an image to composite, a build step, a webhook consumer. So does any browser under Referrer-Policy: no-referrer, which plenty of privacy-conscious sites set globally. So does a sandboxed iframe, which sends the literal string Origin: null.

Allow it, and the bypass is to omit the header — which is less work than forging one.

Practically everyone allows it, and that is the right call: refusing would break real users while barely inconveniencing anyone determined, since they can simply not send the header. But it means you should be clear-eyed about what the check computes. It answers "is this request coming from a site that is not on the list". It does not answer "has this request proved where it came from".

So what is it good for?

One thing, and it is a real thing: stopping another website embedding your images in their pages.

That is the hotlinking case, and it is exactly where the browser's guarantee applies. Somebody puts <img src="your-cdn-url"> on their site; their visitors' browsers faithfully send a Referer naming that site; you refuse. The page author cannot prevent their visitors from telling you the truth. Your bandwidth stops paying for their content.

Do not mistake that for a lock. A domain allow-list is a bandwidth control.

What actually controls access

If a URL must only work for people you have authorised, the request has to carry proof, and the proof has to be something the client cannot manufacture. That means a signature.

A signed URL carries a hash computed from the path, the parameters and an expiry, using a secret only you and the edge hold. The edge recomputes it and compares. Change any part of the URL — a different path, a larger width, a later expiry — and the signature no longer matches, because the client cannot produce a new one without the secret.

Three properties fall out of that, and none of them is available from a header check:

  • Links expire. A leaked URL stops working on a schedule you chose.
  • Parameters are fixed. Somebody who has a link for a 200 px avatar cannot edit it into a request for the 4000 px original.
  • It works everywhere. There is no browser-versus-script distinction to reason about, because nothing depends on the client volunteering the truth.

Use both, for different jobs

They are not alternatives. Turn the allow-list on for public images to keep other sites from serving their pages on your bill. Sign the URLs of anything that is genuinely not meant to be public. And keep your API keys narrowly scoped, because when one leaks, the domain list is not what saves you.