Under 'strict-dynamic', your dashboard's one nonced bootstrap computes every widget's script URL at runtime - what is that policy worth?
answer
- the header stopped bounding destinations
- a code path replaced a hostname list
- trust is now propagated, not declared
- audit the loader and all its inputs
- worth equals the review of computed URLs
basics
~20 sExactly as much as the audit of every script URL that code can compute. The keyword moves the trust decision out of the policy and into the loader, so the policy no longer bounds any destination the loader is willing to fetch.
solid answer
~40 sIt is worth the review of one code path, not of a hostname list. Under `'strict-dynamic'` the scheme and host sources beside the keyword stop contributing, so what the page may load is decided by whatever the bootstrap computes - a base path from a configuration record, an identifier from a manifest, a stored preference. Two things still survive and they are real: script the parser produced from markup in the document does not inherit the trust, and an attacker who cannot read the response cannot supply a matching nonce-source value, so ordinary injection still fails. What you traded away is destination control. The decision is therefore ownership: adopt it when the URL computation and all its inputs are server-controlled and reviewed, and keep an enumerable allowlist when they are not.
code
pseudocode · 6 linesfunction bootWidgets(manifest)
for each widget in manifest.widgets
url = manifest.assetBase + "/" + widget.id + ".js"
element = create script element
set element source to url
insert element into the documentgo deeper
Recall that this keyword lets already-trusted script bring in more script, and that the list of allowed hosts stops doing the work once it is present.
Explain where the decision moved to: from source expressions in the response header to the application code that builds the URL, and name what that code's inputs are.
Trace the real failure: an input reaches the URL computation, the loader fetches attacker-chosen script, and no header anywhere would have stopped it. Then name the compensating controls.
Own the bet. Decide whether the organisation can hold one code path to header-grade review across every property, and say what evidence would send you back to an enumerable allowlist.
## What the keyword actually moves `'strict-dynamic'` does not make a `Content-Security-Policy` stricter or looser in general. It **moves the decision**. An allowlist policy answers the question *may script come from there?* by comparing a URL against source expressions the author wrote. A policy built on `'strict-dynamic'` answers it by asking *was this script inserted by script that was already trusted?* - and beside the keyword, the scheme sources, host sources and `'self'` in the same list stop contributing to that answer. The set of URLs the page may load is therefore whatever the trusted bootstrap decides to build, and nothing in the header constrains it. On a usage dashboard whose chart, export and comparison widgets are fetched at runtime, the loader typically concatenates a base path, an identifier and a suffix. Every one of those three is an input, and every one of them is now part of the security boundary. ## Two things that still survive 1. **Parser-produced script gets nothing.** Markup injected into the document does not receive the propagated trust, so the classic injected `<script src=...>` still fails. 2. **The seed is unguessable.** An attacker who cannot read the response cannot produce a matching nonce-source value, so an injected inline block has no way to become the trusted seed. That is a real defence, and it is the reason the pattern exists at all. The point is that neither of those bounds *destinations*. ## What you traded away | | Allowlist policy | Propagated-trust policy | |---|---|---| | What bounds destinations | the source expressions in the header | the code that computes the URL | | What you must audit | the hostname list, and every endpoint on those hosts | the loader and every input that reaches it | | How it fails | an allowed host serves attacker-chosen script | an input reaches the URL computation | | Who owns the review | whoever edits the header | whoever edits the loader, and whoever can write its inputs | The second column is not worse than the first - it is a different bet. An allowlist that names a handful of large hosts is often worth less than it looks, because anything those hosts will serve is in scope. A propagated-trust policy concentrates the whole question into one auditable code path. It is a good trade **when that path is genuinely auditable**, and a bad one when it is fed by data other people can write. ## How to decide 1. **Enumerate the inputs to the URL computation.** Base path, identifier, version, tenant, locale. For each, name who can write it and through which surface. 2. **Ask whether any of them crosses a trust boundary.** A tenant-editable configuration record, a value round-tripped through a query string, a stored user preference: any of these turns the loader into an unbounded script source, and the policy will not tell you. 3. **Ask whether the destination set is small and stable.** If three hosts cover it and they will still cover it next year, an allowlist is the cheaper control and the more legible one. 4. **Ask who reviews the loader.** A policy header changes under security review; a loader changes under ordinary feature review. If nobody has flagged that file as security-relevant, the control does not exist in practice. 5. **Ask what else constrains the bytes.** Pinning the expected digest of a subresource bounds *what runs* even when the URL is attacker-influenced, which is a different control and a useful companion here. 6. **Decide what evidence would change your mind.** Adoption is reversible, and the migration back to an allowlist is exactly as hard as the destination set is large. ## The honest answer in an interview Say what the policy is worth in one line - the audit of the URL computation - and then say what it still buys. A candidate who claims the keyword prevents cross-site scripting has mistaken a defence in depth for a boundary; a candidate who says it *weakens* the policy has missed that the allowlist it replaces was usually bypassable through an endpoint on an allowed host. The judgment a lead owns is neither of those: it is whether the organisation can keep one code path under the same discipline it applied to a header, across every property, for as long as the policy stands.
- What class of input turns a loader like this into an unbounded script source?Any input that reaches the URL computation and is not server-controlled and reviewed: a tenant-editable base path, an identifier round-tripped through the address bar, a stored preference, a field in a manifest someone else can publish. The policy cannot see any of them.
- What does the policy still prevent once you adopt the keyword?Script the parser produced from markup in the document is not covered by the propagated trust, and an injected inline block cannot carry a matching nonce-source value, so ordinary injection still fails to execute. The gain is genuine; it is the destination allowlist you have given up.
- When would you keep an allowlist instead?When the destination set is small, stable and genuinely enumerable, and when the loader's inputs cross a boundary you do not control. A short host list you can defend beats a code path nobody reviews, and it is far easier for the next engineer to read.
- Does the keyword constrain style or connection destinations too?No. It is defined for script types, so style and other fetch destinations keep whatever their own directives say. Assuming a single keyword hardened the whole policy is a common and expensive misreading.
A badge holder may sign in any guest they choose. The door list has stopped bounding who is inside; what bounds it is the badge holder's judgment, so that is what you audit.
saying these in an interview costs you the question
- Claims the keyword makes a page immune to cross-site scripting
- Thinks the host allowlist still bounds a loader on a Level 3 client
- Treats a widget manifest as trusted because it arrived over TLS
- Says an inserted widget script needs its own nonce to run
- Assumes the keyword also constrains style or connection destinations
- Adopts it without naming who reviews the loader