A web application can carefully encode output and still acquire an injection bug later through a new template, a third-party component, or unsafe client-side code. If attacker-controlled text reaches a place where the browser interprets it as JavaScript, the result can be cross-site scripting (XSS): code runs in the security context of the application and can act with whatever authority the page already has.
The primary fix is to stop untrusted data from becoming executable code. Content Security Policy (CSP) adds a second boundary. The server sends a policy that tells the browser which scripts are allowed to execute. A well-designed policy can therefore reduce the impact of some XSS flaws even when the application accidentally places attacker-controlled markup into a page.
This article develops a practical mental model for CSP, explains nonce- and hash-based script policies, shows what the control does and does not guarantee, and describes how to introduce it without weakening the policy merely to make errors disappear.
Treat CSP as an execution boundary, not an input filter
CSP does not inspect application data and decide whether that data is malicious. It constrains what the browser may execute or load after the browser receives a response.
That distinction matters. Imagine a page that intends to render a display name as text but accidentally inserts it as HTML. Without another control, an injected script element may become executable page content.
The application has already made the first mistake: it interpreted data in the wrong context. CSP can add a second decision:
application output
|
v
browser parses document
|
+--> script has trusted CSP evidence? --> execute
|
+--> no trusted evidence -------------> blockFor a strict script policy, the trusted evidence is normally a nonce or a hash.
A nonce is an unpredictable value generated for one response. The server places it both in the CSP header and on script elements that the response intentionally authorizes. A hash instead identifies the exact bytes of an authorized script. The browser can compare what it sees in the document with the policy before allowing execution.
The important security property is not that the browser knows which script is “good.” The browser only enforces a rule supplied by the application. Security therefore depends on the application granting that evidence only to scripts it intends to trust.
State the threat model before choosing a policy
CSP is especially useful as defense in depth against script injection. Under this threat model, an attacker can influence some HTML or DOM content but cannot also change the CSP response header or obtain whatever evidence the policy requires for trusted scripts.
A strict policy can make several common execution paths unavailable. An injected inline script without the required nonce or hash can be blocked. Inline event-handler attributes such as onclick are also blocked unless the policy deliberately permits them. String-to-code mechanisms such as eval() are unavailable unless the policy enables them with a keyword such as 'unsafe-eval'.
CSP does not repair the injection flaw itself. The unwanted markup can still exist in the document. That may matter even if it cannot execute JavaScript. An attacker might alter visible content, create misleading links or forms, or abuse capabilities that the policy still allows.
CSP also cannot protect a page from code that the policy intentionally trusts but that is itself unsafe. If a nonce-bearing application script reads attacker-controlled data and passes it to a dangerous execution sink, the script already has permission to run. The browser does not understand the application’s data-flow intent.
Finally, CSP assumes the attacker cannot rewrite the response header. If the server, reverse proxy, or another trusted component that sets the policy is compromised, CSP is not an independent boundary against that component.
So the defensive model is narrow but valuable:
prevent injection in application code
+
limit which injected code the browser may executeThe first control reduces the chance of the flaw. The second can reduce the consequence of some flaws that remain.
Start with the smallest nonce-based example
Consider a server-rendered page that needs one script. For teaching purposes, suppose the server generates the random value r4nd0m-example for this response. A real nonce must come from a cryptographically secure random generator and must not be a fixed example value.
The response can contain a policy such as:
Content-Security-Policy: script-src 'nonce-r4nd0m-example'; object-src 'none'; base-uri 'none'The intended script carries the same value:
<script nonce="r4nd0m-example" src="/assets/app.js"></script>When the browser evaluates the script, its nonce matches the nonce source in script-src, so that script is eligible to execute.
Now suppose an injection flaw causes an additional script element to appear in the page:
<script>unexpectedCode()</script>It has no matching nonce. Under the policy above, the browser blocks it. The injected markup is still evidence of a bug, but the CSP changes the likely consequence by withholding permission to execute that script.
This is why a nonce is better understood as per-response authorization evidence for a script element, not as a secret that authenticates a user.
Generate a fresh nonce for each response
A nonce should be unpredictable and generated anew for each response that uses a nonce-based policy. Reusing a predictable or fixed value defeats the trust decision: if an attacker knows a nonce that will be accepted in a future page, an injection flaw may let the attacker attach that accepted value to injected script markup.
Generate the value on the server with a cryptographically secure random generator, encode it in a form suitable for the header and HTML attribute, and pass the same value through the rendering path for that response.
Do not solve nonce deployment by post-processing arbitrary HTML and adding the nonce to every script element. If attacker-injected script markup passes through the same process, the application would grant its own trust evidence to the content it is trying to constrain.
The trust decision must happen where the application can distinguish scripts that belong to the template or rendering logic from markup that came from untrusted data.
Use hashes when the authorized script is stable
A hash-based policy provides a different form of evidence. Instead of generating a value per response, the policy contains a cryptographic hash of the exact authorized script content.
Conceptually:
authorized script bytes
|
v
SHA-256
|
v
base64 hash in CSPWhen the browser encounters the script, it computes the relevant hash and compares it with the policy. If the values match, the content is authorized.
CSP supports SHA-256, SHA-384, and SHA-512 hash source expressions. The hash is sensitive to the exact script content, including whitespace. Change the script and its hash changes, so the policy must be updated as part of the same release.
That property makes hashes convenient for static content whose bytes are known at build time. Nonces are often easier when HTML is generated dynamically and the server can insert a fresh value into both the response header and intended script elements.
Neither mechanism is inherently a universal replacement for the other. The useful question is where the application can most reliably bind authorization to intended code without creating a bypass-prone maintenance process.
Understand what strict-dynamic changes
Modern applications often have an authorized bootstrap script that loads other scripts dynamically. Requiring every later script URL to appear in a host allowlist can be difficult to maintain and can accidentally turn host membership into the security decision.
The 'strict-dynamic' source expression changes that model. When used with a nonce or hash, trust given to an authorized root script can propagate to scripts that root script loads dynamically. In browsers that implement this behavior, host and scheme source expressions such as 'self' are ignored for that script-loading decision.
A policy may therefore look like:
Content-Security-Policy: script-src 'nonce-r4nd0m-example' 'strict-dynamic'; object-src 'none'; base-uri 'none'The mental model becomes:
nonce/hash authorizes root script
|
v
trusted root deliberately loads another script
|
v
trust can propagate through strict-dynamicThis can make a strict CSP practical for applications with script loaders. It also means the root script becomes an important trust boundary. If trusted code can be manipulated into loading an attacker-selected script, CSP does not make that behavior harmless. The policy deliberately delegates loading authority to trusted code.
Use 'strict-dynamic' when that delegation matches the application’s script architecture, not merely because it makes CSP violations disappear.
Do not turn CSP into a broad origin allowlist
A tempting first policy is to allow scripts from 'self' and a collection of trusted domains. That can provide some protection, but origin trust is often much broader than code trust.
Consider 'self'. It means a matching same-origin source can satisfy that source expression. If the same origin contains a user-controlled upload, a JSONP-like endpoint, an old script gadget, or another path that can return attacker-influenced JavaScript, trusting the entire origin may admit more executable content than intended.
Third-party domains create a similar problem. A domain can host many resources, change ownership or behavior, or expose endpoints with different trust properties. Adding domains whenever something breaks gradually converts the policy into a record of dependencies rather than a strong execution boundary.
A nonce- or hash-based strict policy asks a narrower question: did this script receive explicit authorization from this response or build? That tends to align better with the actual decision the browser needs to make.
This does not mean host restrictions are useless. They can still be appropriate for resource types and compatibility needs. The point is to avoid assuming that a long script-source allowlist provides the same XSS resistance as explicit script authorization.
Avoid the two escape hatches that erase key protections
CSP deployment often exposes existing application patterns that the policy intentionally rejects. Two common reactions are to add 'unsafe-inline' or 'unsafe-eval'.
'unsafe-inline' broadly permits inline JavaScript when it is effective in the policy. That includes a major class of content a strict script policy is intended to reject. If the application depends on inline scripts, prefer authorizing specific script elements with a nonce or exact script content with a hash rather than enabling arbitrary inline execution.
'unsafe-eval' permits JavaScript evaluation mechanisms that are otherwise blocked by the script policy, including eval() and related string-to-code behavior. If application or dependency code requires it, treat that as an explicit reduction in protection. Determine whether the dependency can be configured, upgraded, or replaced before accepting the weaker boundary.
The operational lesson is important: a CSP violation is not automatically a reason to loosen CSP. It is evidence that the application’s current execution behavior and intended policy disagree. First decide which side is wrong.
Deploy in report-only mode before enforcing
A strict policy can break legitimate application behavior if the current page relies on scripts that the policy does not authorize. For an established application, it is usually safer to observe that disagreement before blocking users.
Browsers support a report-only form of CSP through the Content-Security-Policy-Report-Only response header. A report-only policy can surface violations without enforcing the corresponding restrictions. This makes it useful for discovering dependencies and unsafe patterns during rollout.
A practical deployment sequence is:
- Define the policy you actually want, rather than a permissive policy that merely matches current behavior.
- Send it in report-only mode in a production-like environment and, where appropriate, a controlled portion of production traffic.
- Exercise important user flows, including authentication, payments, account changes, error pages, embedded content, and rarely used administrative paths.
- Investigate violations. Authorize intended scripts deliberately; refactor unnecessary inline handlers or string-to-code behavior rather than automatically adding escape hatches.
- Enforce the policy once legitimate execution paths are represented correctly.
- Continue testing the enforced policy as templates and dependencies change.
Violation reporting can help find gaps, but reports are telemetry rather than proof that a policy is complete. Clients may not send reports, extensions can create noise, and untested paths can remain invisible. Automated browser tests that verify important pages under enforcement provide a separate check.
Verify the control with security invariants
A CSP is easier to maintain when tests express the security properties you expect instead of only checking that pages render.
For a nonce-based strict policy, useful invariants include:
- every response that needs authorized scripts receives a fresh nonce;
- the CSP header and intended script elements use the same nonce for that response;
- untrusted rendered data never receives a nonce merely because it contains a script element;
- required application flows work without
'unsafe-inline'or'unsafe-eval', unless a documented threat-model decision explicitly accepts one; - injected inline script without trusted evidence does not execute in browser tests;
- pages that do not need JavaScript do not gain script permission unnecessarily.
Also test the response that users actually receive. A correct application header can be changed, duplicated, or removed by a proxy, CDN, middleware layer, or error handler. Check normal pages, redirects where relevant, and application-generated error responses rather than assuming one framework configuration covers every path.
If the application caches HTML, include nonce behavior in the cache design. Serving the same nonce-bearing document repeatedly can turn a per-response value into a reusable one. Depending on the architecture, generate the nonce after the relevant cache boundary, avoid caching personalized nonce-bearing HTML, or choose a hash-based approach for static documents.
Know the residual risks
A strong CSP changes browser execution rules; it does not make unsafe rendering acceptable.
Continue to use context-appropriate output encoding so text remains text. Sanitize HTML when the product intentionally accepts a limited HTML subset. Avoid dangerous DOM APIs when safer APIs can express the same operation. Keep dependencies and trusted scripts within the application’s security review, because CSP grants those scripts significant authority once they are allowed to execute.
Also remember that script execution is only one browser capability. Depending on the application, a complete policy may need directives for frames, form submission, network connections, images, styles, and other resources. Those decisions deserve their own threat models. Do not add directives mechanically and assume a longer header is stronger.
For a small static site with no JavaScript, the simplest useful control may be to disallow scripts entirely rather than introduce nonces. For a dynamic application with a small known script set, nonce- or hash-based authorization can be straightforward. For a large application with loaders and third-party code, a strict policy may require architectural work and careful testing, but the same principle still applies: make script trust explicit and narrow.
Conclusion
Content Security Policy is most useful when it is treated as a browser-enforced execution boundary behind the application’s primary XSS defenses.
Start from the threat model: attacker-controlled content may reach a page, but it should not automatically gain permission to execute as JavaScript. Authorize intended scripts with fresh per-response nonces or stable hashes, use trust propagation only when the script architecture requires it, and resist broad escape hatches that restore the behavior the policy was meant to constrain.
Then verify the boundary in the browser, not only in configuration. A good CSP does not prove that the application has no injection bugs. It gives some of those bugs one more security decision to cross before they become script execution.