skip to content

A state-changing request arrives with neither an Origin nor a Referer request header — what should the server do, and why?

level: seniorimportance: should knowfreq 46%

answer

  1. unverifiable is a decision, not a gap
  2. truncation is fine, absence is not
  3. prohibited on a secure-to-unsecured request
  4. parse the fallback, compare its origin
  5. refuse on provenance, not on credentials

basics

~20 s

Refuse it. With neither field the initiator is unverifiable, and allowing an unverifiable request is exactly the fall-through a forged request needs. Refusing also proves the check cannot be the whole defence, since legitimate traffic can land there.

solid answer

~50 s

`Referer` is the weaker second try, not a rescue. Its truncation is not the problem — the default referrer policy `strict-origin-when-cross-origin` cuts a cross-origin value down to the referring origin, and an origin is all this comparison needs. The problem is that it can be gone entirely: a `no-referrer` policy suppresses it, a user agent **MUST NOT** send it in an unsecured request when the referring page was received securely, it **MAY** arrive as `about:blank` when the target URI had no URI-bearing source, and intermediaries strip it. So "neither field arrived" is a normal state with no information in it, and the only safe resolution is to refuse the unsafe request with `403 Forbidden`. That refusal is also the argument that this check is a second signal: a defence that legitimate traffic can fail is not one you can stand on alone.

code

pseudocode · 17 lines
pseudocode
function verify_initiator(request):
    origin = request.header("Origin")
    if origin is present:
        if origin equals "null":
            return REFUSE          // present, but identifies nothing
        if ALLOWED contains origin:
            return PROCEED
        return REFUSE              // a document we did not serve

    referer = request.header("Referer")
    if referer is present and referer parses as a URI:
        candidate = origin_of(referer)   // scheme, host, port only
        if ALLOWED contains candidate:
            return PROCEED
        return REFUSE

    return REFUSE                  // nothing arrived: fail closed

go deeper

for a junior

Remember the rule: if the server cannot tell where an unsafe request came from, it refuses it. Allowing the request because a header was missing is how these checks are defeated.

for a middle

Explain why Referer is the weaker fallback — suppressed by policy, prohibited on a secure-to-unsecured request, sometimes about:blank — and why its truncation to an origin costs this check nothing.

for a senior

Demonstrate the operational half: parse rather than string-match the fallback, return the provenance rejection rather than the credential one, and read a spike in unverifiable requests as your own deployment first.

for a principal

Take the position that a check legitimate users can fail cannot carry the risk alone, and say where the residual risk sits and what you would accept breaking to keep the refusal strict.

## Why Referer is the fallback and not the answer When the `Origin` field is absent, `Referer` is the only other thing a user agent volunteers about where a request came from. It is worth trying, and it is worth understanding precisely why it is weaker. The weakness people name first is usually wrong. **Truncation is not the problem.** RFC 9110 permits a user agent to trim the field down to the referring origin, and the default referrer policy, `strict-origin-when-cross-origin`, does exactly that for cross-origin requests. A comparison that only wants to know *which origin initiated this* is perfectly served by a value trimmed to that origin. Truncation removes the path, which this check never needed. The real weakness is that the field is **frequently absent altogether**: - A `Referrer-Policy` of `no-referrer` suppresses it entirely. - A user agent **MUST NOT** send it in an unsecured request when the referring page was received over a secure protocol — RFC 9110 states that as a prohibition, not a recommendation. - It **MAY** be sent as `about:blank` when the target URI had no URI-bearing source, which is a value carrying no origin to compare. - Intermediaries and privacy tooling strip it, and always have. So a request with neither field is an ordinary event, not a suspicious one, and it carries no information at all. ## Comparing Referer correctly when it is there It is a URI reference, not a serialized origin, so the two comparisons are not the same: 1. Parse the value as a URI. Refuse the decision outright if it does not parse, or if it is `about:blank`. 2. Take its scheme, host and port and build the origin from them. 3. Compare that origin byte-exactly against the same allowlist the `Origin` field is compared against. Never string-match the raw value. `https://booking.example.com.evil.test/page` starts with your origin exactly as it does in the other field, and here the trailing path makes a prefix test feel even more natural — and even more wrong. ## The decision when nothing arrived Fail closed. The request is unsafe, it carries ambient credentials the user agent attached on its own, and nothing in it establishes who set it in motion. The alternative — treat "unverifiable" as "probably fine" — is the single fall-through a forged request needs, and an attacker who can suppress a header simply takes it. | State | Decision | |---|---| | `Origin` present and on the allowlist | proceed to the real defence | | `Origin` present and not on the allowlist | refuse | | `Origin` present as `null` | refuse | | `Origin` absent, `Referer` origin on the allowlist | proceed | | `Origin` absent, `Referer` origin not on the allowlist | refuse | | both absent | refuse | Return `403 Forbidden`, not `401 Unauthorized`: the credential was valid and was accepted, and it is the request's provenance that failed. A `401` tells the client to go and obtain credentials, which sends a logged-in user into a loop that cannot resolve the problem. ## What failing closed then tells you about the check Here is the honest consequence, and it is the point of the whole leaf. Failing closed refuses **some legitimate traffic**: a page hardened with `no-referrer`, a client behind an intermediary that strips fields, a path that downgrades to an unsecured scheme. A defence that legitimate users can fail through no fault of their own is not a defence you can stand on by itself. So the initiator check earns its place as corroboration: - It is stateless, costs nothing, and refuses the ordinary cross-site forgery before any handler runs. - It catches requests that arrive carrying an origin you have never heard of, which is the loudest possible signal. - It does **not** cover the state-changing GET, the `null` value, or the request with no fields, and each of those resolves to a refusal rather than a pass. - The load-bearing defence remains a value the cross-site document cannot read and therefore cannot replay. And instrument it. A rising rate of refusals with no fields present is far more likely to be your own deployment — a new policy header, a new intermediary — than an attack, and you want to see the shape of that traffic before someone proposes making the check permissive to stop the noise.

  • Does the default referrer policy make Referer useless for this check?
    No. `strict-origin-when-cross-origin` truncates a cross-origin value to the referring origin, and the origin is the only part this comparison uses. Truncation costs you the path, which the check never wanted. What defeats the fallback is the field being absent entirely, or arriving as `about:blank`.
  • Why compare the origin parsed out of Referer rather than string-matching the value?
    Because the value is a full URI reference with a path, so a prefix test against your origin accepts `https://booking.example.com.evil.test/page`, a host the attacker registered. Parse the value, build the scheme/host/port origin from it, and compare that byte-exactly against the same allowlist.
  • Refusals with no Origin and no Referer are climbing sharply. Is that an attack?
    Usually not. A new `Referrer-Policy` on a page, a new intermediary that strips fields, or a path that downgrades to an unsecured scheme all produce it, and each is your own deployment. Investigate the shape of the traffic before anyone proposes making the check permissive to quiet the noise.
  • If the check can refuse legitimate traffic, why run it at all?
    Because it is stateless, runs before any handler, and refuses the ordinary cross-site forgery outright — a request arriving with an origin you have never heard of is the loudest signal you will get. It is corroboration with known blind spots, sitting beside a value the cross-site document cannot read.

saying these in an interview costs you the question

  • Allows the request when no Origin and no Referer arrived.
  • Says the default referrer policy strips Referer rather than truncating it.
  • Assumes a truncated Referer is useless for comparing origins.
  • String-matches the raw Referer value instead of parsing out its origin.
  • Treats an absent Referer as evidence the request is hostile.
  • Relies on the initiator check alone and ships no unguessable token.