skip to content

In a Content-Security-Policy, why does a redirect on an allowlisted host defeat a `script-src` path restriction?

level: seniorimportance: nice to knowfreq 33%

answer

  1. paths stop being compared after a hop
  2. scheme, host and port still checked
  3. the rule prevents an information leak
  4. one redirector opens every path
  5. a path-part is intent, not a boundary

basics

~20 s

Path matching is abandoned once a request has been redirected: only the scheme, host and port of the redirect target are matched. A redirector under an allowed path therefore reaches any path on any host the policy allows.

solid answer

~50 s

A `host-source` may carry a `path-part`, and on a directly fetched URL that path is matched. After a redirect it is not: the matching algorithm deliberately stops comparing paths once the request has been redirected, so only `scheme-part`, `host-part` and `port-part` are checked on the target. The rule exists so that a page cannot probe a cross-origin redirect's path by watching whether the load was refused. The consequence for an audit is that any redirector reachable under an allowed URL turns the path restriction into decoration - including a redirector that lands on a user-uploaded file elsewhere on the same host. State the limit too: every hop is still matched, so a redirect to a host or scheme nowhere in the source list is still refused. A `path-part` is a statement of intent, not a boundary.

code

http · 5 lines
http
GET /lib/r?u=/uploads/u9931/payload.js HTTP/1.1
Host: static.example.net

HTTP/1.1 302 Found
Location: /uploads/u9931/payload.js

go deeper

for a junior

The takeaway is simple: a path written into a source expression only constrains the first request, so it is not the thing keeping a script out.

for a middle

Explain which parts of the expression survive a redirect - scheme, host and port are matched, the path is not - and why the specification drops the path on purpose.

for a senior

Turn it into a finding: any redirector reachable under an allowed URL grants every path on every allowed host, so audit hosts for redirect endpoints and user-writable content rather than trusting the path text.

for a principal

The broader call is whether a policy whose safety depends on other teams' URL space is worth maintaining at all, given that a redirect added months later silently widens it with no change to your header.

## The rule A source expression may be more than a host. `https://static.example.net/lib/` carries a `scheme-part`, a `host-part` and a `path-part`, and on a directly fetched URL all of them are matched - a script at `/uploads/x.js` on that host does not match, and is refused. The moment the request is redirected, that changes. **Path matching is abandoned for a redirected request**: the target is matched on scheme, host and port only. A URL that would have been refused as a first-party fetch is permitted as the end of a redirect chain. ## Why the rule exists This is deliberate, not an oversight. If the path of a redirect target were matched, a page could learn that path without ever reading the response. It would write a policy with a narrow path, point a script element at a cross-origin redirector, and watch whether a violation was raised - turning the policy into an oracle for redirect targets that frequently encode user identifiers, session state or account routing. Dropping the path check closes that leak. The price is paid by anyone who thought a `path-part` was an access-control boundary. ## What is still checked This is where over-claiming is easy, so keep the limit explicit: - **Every hop is still matched against the policy.** A redirect does not exempt the chain from the directive. - **The scheme still has to match.** A target on `http://` does not satisfy an expression whose `scheme-part` is `https`. - **The host still has to match.** A redirect to a host that appears nowhere in the source list is refused. - **The port still has to match** where the expression names one. - **Only the path stops being compared.** So an open redirect does not hand an attacker the whole internet. It hands them **every path on every host the policy already allows** - which, on a policy whose safety rested on narrow paths, is the whole finding. ## The attack shape on the statement viewer 1. The policy is `script-src 'self' https://static.example.net/lib/`, written narrowly on purpose because that host also serves user-uploaded files under `/uploads/`. 2. Somewhere under `/lib/` sits a redirector - a link tracker, a locale switcher, a versioned-asset resolver - that takes a target in its query string. 3. The attacker injects `<script src="https://static.example.net/lib/r?u=/uploads/u9931/payload.js">`. 4. The first request matches the expression completely, path included, so it is permitted. 5. The redirect target is matched on scheme, host and port only - all of which match - so the uploaded file loads and runs in the statement page's origin. Nothing in that chain broke a rule. The policy did exactly what it specifies. ## What a path-part is worth | | Directly fetched URL | Target of a redirect | |---|---|---| | `scheme-part` | matched | matched | | `host-part` | matched | matched | | `port-part` | matched | matched | | `path-part` | matched | **not matched** | Read that table as the audit rule: a `path-part` constrains only the URL a document asks for first. It is worth writing - it documents intent and it does stop a direct fetch of the wrong file - but it cannot be relied on as the thing separating your script from somebody's upload. Two practical consequences follow for the statement page: - **A host that serves arbitrary user content anywhere is granted in full**, whatever path you wrote, as soon as any redirector is reachable on it. - **A host that can redirect is a host you have allowlisted for all of its paths**, so "we only allowed one directory on it" is not a finding you can close on the strength of the policy text alone. The defensible position is the one this whole leaf keeps arriving at: allowlists describe where code came from, and where code came from is a weak proxy for whether it should run.

  • Can the redirect land on a host the policy never listed?
    No. Every hop is still matched against the source list, and only the `path-part` stops being compared - `scheme-part`, `host-part` and `port-part` are all still checked on the target. A redirect to an unlisted origin, or from `https` to `http` under an expression that names `https`, is refused.
  • What should an auditor conclude about writing paths into a source list?
    Write them, but do not count them. A `path-part` holds for the first request only, so treat any host that can redirect, or that hosts content other people control, as allowlisted in full. If the narrow path was the reason the host looked acceptable, the finding is the host, not the path.
  • Does the same rule apply to the initial request in the chain?
    No - the first request is matched with its path, so a script element pointing straight at the excluded path is refused as expected. That is exactly why the redirector has to be reachable under an allowed URL for the bypass to work at all.

saying these in an interview costs you the question

  • Thinks a redirect lets a script load from any host
  • Says the final URL is matched exactly like the first
  • Believes a narrow path in the list is an access boundary
  • Claims the redirect chain is exempt from the directive
  • Assumes dropping the path check was an oversight