Content Security Policy for XSS: Nonces, Hashes, strict-dynamic, and Report-Only

Learn how CSP reduces XSS risk using nonces, hashes, strict-dynamic, and Report-Only, with practical rollout steps, examples, common mistakes, and validation guidance.

Back to Blog

Content Security Policy for XSS: Nonces, Hashes, strict-dynamic, and Report-Only

Content Security Policy, usually shortened to CSP, is one of the most useful browser-side defenses for reducing the impact of cross-site scripting. It is also frequently misunderstood. A CSP does not make unsafe rendering safe, and it does not replace output encoding, sanitization, or secure framework behavior. Its job is different: constrain which resources the browser is allowed to execute and where those resources can come from.

A weak policy can create a false sense of security. A strong policy can materially reduce exploitability, but only when it is designed around the application's actual script-loading model. This guide explains nonces, hashes, strict-dynamic, Report-Only deployment, common failure modes, third-party scripts, and a practical rollout process.

The examples focus on defensive configuration. A production policy should always be tested against the actual application rather than copied from an example unchanged.

CSP is defense in depth, not an XSS fix

If user input reaches innerHTML unsafely, the application still has an injection flaw even if CSP blocks one demonstration. The correct remediation is to fix the unsafe sink. CSP then limits the damage if another injection path is introduced later.

This distinction matters operationally. Teams should track the root-cause vulnerability and the browser policy separately. Otherwise a future CSP change can silently turn a previously blocked injection into an exploitable one.

How CSP constrains script execution

CSP is usually delivered through the Content-Security-Policy response header. Directives describe which origins, schemes, nonces, hashes, or other conditions are allowed for different resource types. For XSS mitigation, script-src is central.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM_VALUE';
  object-src 'none';
  base-uri 'none'

This example is intentionally simplified. Real applications may need additional directives for APIs, images, fonts, styles, workers, frames, and reporting endpoints.

Nonces

A nonce is a random value generated for a response and attached both to the CSP header and to approved script elements. The browser allows a script with the matching nonce and blocks an inline script that lacks the approved value.

Content-Security-Policy: script-src 'nonce-a8f4c1...'

<script nonce="a8f4c1..." src="/app.js"></script>

The nonce must be unpredictable and generated appropriately for each response. A static nonce copied into every page defeats much of the model because attacker-controlled markup can reuse a known value.

Common nonce deployment mistakes

  • Using the same nonce on every response.
  • Adding a nonce to a script block that itself interpolates untrusted data.
  • Forgetting framework-generated scripts and causing inconsistent behavior.
  • Assuming a nonce sanitizes HTML or JavaScript data.
  • Generating the nonce correctly at the origin but losing the header at a CDN or reverse proxy.

Hashes

Hashes allow specific inline scripts to execute when their content matches a cryptographic digest listed in the policy. They are useful for stable inline snippets that do not change between responses.

Content-Security-Policy:
  script-src 'sha256-BASE64_HASH_VALUE'

Hashes are less convenient for dynamic inline code because even a small content change alters the digest. Build tooling can automate hash generation, but teams should understand how deployments update the policy so stale hashes do not break production.

What strict-dynamic changes

strict-dynamic changes the script trust model in supporting browsers by allowing a nonce- or hash-trusted script to load additional scripts programmatically. This can help applications that use trusted bootstrapping code or script loaders.

The directive should not be copied blindly. Test compatibility with the browsers you support, understand how scripts are loaded, and keep sensible fallback expressions where required. A policy that nobody on the team understands will be difficult to maintain safely.

Use Report-Only before enforcement

A practical CSP rollout often starts with Content-Security-Policy-Report-Only. The browser evaluates the policy and reports violations without blocking resources. This lets the team discover what an enforcement policy would break.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-...';
  object-src 'none'

Report-Only is not protection. It is a deployment tool. Once legitimate violations are understood and the policy is stable, move toward an enforced policy.

Hypothetical scenario: rolling out CSP on a SaaS dashboard

A SaaS dashboard loads its main bundle from the same origin, analytics from one vendor, customer support from another, and several legacy inline scripts. The team tries script-src 'self' in enforcement mode and immediately breaks analytics, support functionality, and a login helper.

A better rollout begins by inventorying scripts and removing unnecessary inline blocks. The team introduces per-response nonces for required framework scripts, tests third-party dependencies, and runs a Report-Only policy. Violations are grouped into legitimate resources, browser extensions, stale code paths, and unexpected execution.

After the legitimate dependencies are understood, enforcement is enabled in a controlled release. The final policy is narrower than the original allowlist because the rollout forced the team to understand which script sources were actually necessary.

Third-party scripts complicate CSP

Allowing an entire third-party origin means any script available from that allowed origin can become relevant to the trust model. Prefer tightly scoped integrations, reduce unnecessary third-party code, and understand whether a vendor uses dynamic subdomains or additional script loaders.

CSP is not a software supply-chain control by itself. If an explicitly trusted third-party script is compromised, the browser may still execute it because the policy considers the source trusted. Dependency governance, Subresource Integrity where appropriate, vendor review, and minimizing third-party JavaScript remain important.

Why unsafe-inline weakens XSS protection

The 'unsafe-inline' keyword permits broad categories of inline script that a stricter nonce- or hash-based policy is designed to constrain. Some legacy applications still depend on it, but teams should treat it as migration debt rather than a comfortable endpoint.

Do not remove it without testing. Instead, identify which inline behavior requires it and migrate that behavior deliberately. Moving logic into external scripts or nonce-approved blocks can reduce dependence on broad inline execution.

Common weak CSP patterns

  • Policies that rely on 'unsafe-inline' indefinitely.
  • Very broad host allowlists.
  • Wildcard sources added for convenience.
  • Policies present in application configuration but missing on the public response.
  • Report-Only policies that never progress to enforcement.
  • Policies that block a proof payload while leaving the underlying XSS sink unfixed.
  • Different policies across CDN, proxy, and origin paths without deliberate design.
  • A single global policy that does not fit sensitive application areas.

How to build a CSP for XSS reduction

Step 1: Inventory executable content

Identify first-party bundles, inline scripts, third-party integrations, workers, frames, and dynamically created script elements. Include staging and production differences.

Step 2: Remove unnecessary inline code

Moving stable logic into controlled bundles reduces policy complexity and makes nonce or hash adoption easier.

Step 3: Choose nonces or hashes

Use nonces for dynamic responses and hashes for stable inline code. Some architectures use both. Document the generation process.

Step 4: Deploy Report-Only

Collect violations long enough to understand normal application behavior, but do not treat observation mode as the final security control.

Step 5: Enforce and monitor

Roll out enforcement carefully and continue monitoring because new integrations and releases can weaken or break the policy.

Validate the public policy, not just the config file

Use Vulnify's CSP Checker to inspect the policy actually delivered by the public website. The Security Headers Analyzer provides broader response-header context.

Public validation matters because a correct origin configuration can be changed by a CDN, reverse proxy, hosting platform, or deployment rule before it reaches the browser. Test representative pages, including authenticated areas, checkout flows, embedded pages, and error templates when they use different infrastructure.

CSP alongside XSS remediation

  • Fix unsafe source-to-sink data flows first.
  • Use context-aware output encoding.
  • Sanitize only where raw HTML is genuinely required.
  • Use safe framework rendering defaults.
  • Add CSP as an execution constraint.
  • Consider Trusted Types for large client-side applications.
  • Retest known XSS cases after CSP changes.

Treat CSP as code

CSP changes can break critical application behavior or silently weaken protection. Keep policy definitions in version control where possible, review changes, and test them as part of releases. A request to add a new wildcard should receive the same scrutiny as a request to change an authentication rule.

When a new vendor requires additional script origins, ask whether the integration is necessary, whether a more specific host can be allowed, and whether the vendor loads further dependencies. CSP becomes stronger when the policy reflects intentional architecture rather than accumulated exceptions.

Frequently asked questions

Does CSP stop all XSS?

No. CSP can block many script-execution paths, but effectiveness depends on policy quality and application behavior. It should be one layer in an XSS prevention strategy.

Should I use a nonce or a hash?

Nonces suit dynamic pages where approved script elements vary by response. Hashes suit stable inline scripts. Architecture and framework behavior should drive the choice.

Does Report-Only protect users?

No. Report-Only evaluates and reports policy violations without enforcing them. It is intended for testing and rollout.

Can I copy another site's CSP?

You can learn from examples, but a production CSP must reflect your own resources, frameworks, third parties, and browser requirements. Copying a policy without understanding it can either break the site or provide weak protection.

Conclusion

A strong CSP can make XSS substantially harder to exploit, but only if it is designed around real application behavior and paired with secure rendering. Nonces, hashes, strict-dynamic, and Report-Only are tools for building that policy deliberately. Fix injection first, constrain execution second, validate the public response, and keep the policy under change control as the application evolves.