A script allowlisted by host and path still loads after the request is redirected elsewhere on that host - why?
answer
- matching changes after the first hop
- the redirect count decides
- path-part is skipped, not re-matched
- scheme, host and port still checked
- avoids leaking the redirect target
basics
~20 sPath matching in a Content-Security-Policy source expression applies only while the request has not been redirected. Once the redirect count is above zero the browser skips the path-part and matches scheme, host and port alone.
solid answer
~50 sA host-source can carry a path-part, as in `https://tiles.example.net/v1/`, and on the initial request the browser matches it. But the matching algorithm checks how many redirects the request has already followed: once that count is non-zero, the **path-part is skipped entirely** and only the scheme, host and port still have to match. The reason is a leak. If path matching survived a redirect, the page could tell from whether the resource loaded where a cross-origin redirect had landed, which is information the document is not otherwise allowed to read. Dropping the path is the price of not turning a policy into a probe. The consequence for the author is that a path in a source expression is a hygiene measure, not a boundary: any endpoint on an allowlisted host that redirects can send you to a different path on a host the policy still accepts.
code
http · 5 linesGET /v1/tiles.js HTTP/1.1
Host: tiles.example.net
HTTP/1.1 302 Found
Location: https://tiles.example.net/assets/tiles-9f3c.jsgo deeper
Recall that a source expression may carry a path, and that the path stops being checked once the request has been redirected.
Explain the split by redirect count, and state exactly what still matches afterwards - scheme, host and port, under the ordinary rules.
Reason about the consequence in production: the enforceable unit is the host, so diagnose a refusal by checking for a redirect before re-reading the path, and never sell a path as a boundary.
Take the design lesson - the specification traded precision for not leaking a redirect target, so finer control has to be enforced at the host you allowlisted, not in the header.
## The rule A **host-source** in a `Content-Security-Policy` may carry four parts: a scheme-part, a host-part, a port-part and a path-part. Writing ```http Content-Security-Policy: default-src 'none'; script-src https://tiles.example.net/v1/ ``` says that scripts may be loaded from that host, over that scheme, **under that path prefix**. The matching algorithm, though, also takes the request's **redirect count** into account. The behaviour splits in two: 1. **Redirect count is zero.** The full expression is matched, path-part included. A request for `/v2/tiles.js` against a policy naming `/v1/` is refused. 2. **Redirect count is one or more.** The path-part is **not matched at all**. Scheme, host and port must still match; the path is simply not consulted. So a request that starts at an allowed path and is redirected to another path on the same host is permitted, and so is one redirected to a different allowed host entirely. ## Why the path is dropped This is not an oversight, and it is not laxity. It is there to stop the policy from becoming a **measuring instrument**. A document cannot normally read where a cross-origin redirect chain ended - that is the whole point of the boundary. If a path-restricted source expression were still matched after a redirect, a page could learn something about the redirect target purely from whether the resource loaded: allowlist one path, observe success or refusal, and you have extracted a bit about a location the browser was never going to tell you. Skipping path matching after the first hop removes that channel. The policy gives up some precision so that it cannot be used as an oracle. ## What still holds after a hop It is worth being precise about what is *not* given up: - the **scheme** must still match, with the same upgrade-only comparison as anywhere else - the **host** must still match, wildcard rules included - the **port** must still match - every hop in a chain is subject to this, so a redirect cannot land anywhere the host allowlist does not already cover The allowlist is intact; only its path precision is gone. ## Reading a real exchange ```http GET /v1/tiles.js HTTP/1.1 Host: tiles.example.net HTTP/1.1 302 Found Location: https://tiles.example.net/assets/tiles-9f3c.js ``` Under `script-src https://tiles.example.net/v1/`, the first request matched on all four parts. The second is judged with the path ignored, matches on scheme, host and port, and the script runs from `/assets/`. Nothing malfunctioned. ## What this means for how you write an allowlist Three consequences are worth carrying into a review: - **A path is documentation and hygiene, not a boundary.** It records which part of a host the page is supposed to use and catches an honest mistake on the first request. It cannot confine anything past a redirect. - **Granularity ends at the host.** If you need a real limit on what may be loaded, the unit you get to choose is scheme, host and port. Two different trust levels sharing one host cannot be separated by a path in a policy. - **Diagnose refusals in the right order.** When a load is refused despite an apparently matching path, check whether the request was redirected at all before re-reading the path - after a hop, the path is not why anything failed, and the real mismatch is in the scheme, host or port. ## The general shape of the lesson This rule is a good example of what a policy is and is not. A conforming browser enforces it on the document that received the header; the server only declares it. It restricts **where a document may load things from**, at the granularity the browser can enforce without leaking information back to the page. Where an author wants a finer boundary than that - which part of a host may serve what, to whom - the enforcement has to live at the host itself, because the policy was never able to express it past the first redirect.
- After a redirect, what parts of a host-source does a browser still match?Scheme, host and port, with the usual rules - upgrade-only scheme comparison and host-part wildcards matching subdomains but not the apex. Only the path-part is dropped, and it is dropped for every subsequent hop in the chain, not just the first.
- Is a path in a source expression worth writing at all, given the redirect rule?Yes, modestly. It documents which part of a host the page is meant to use and catches a wrong initial URL on the first request. Treat it as hygiene rather than a boundary, and never rely on it to separate two trust levels sharing one host.
- A load is refused even though its path clearly matches the policy. What do you check first?Whether the request was redirected. If it was, the path is not being matched at all, so the refusal is in the scheme, host or port - a different port, a plaintext scheme, or a host outside a subdomain wildcard. Re-reading the path wastes the first hour.
saying these in an interview costs you the question
- Thinks a path in a source expression confines loads after a redirect
- Says the policy is re-parsed at each redirect hop
- Believes the host allowlist is also ignored after a hop
- Treats the dropped path matching as a browser bug
- Uses a path to separate trust levels on one shared host