Web Development

Content Security Policy (CSP): Practical Engineering Guide

Learn how Content Security Policy works, how strict CSP uses nonces and hashes, how to deploy it safely, diagnose violations, and avoid common CSP mistakes.

SeoNest Team2 min read
Open article contents

Content Security Policy: A Practical Engineering Guide

Content Security Policy (CSP) is a browser-enforced security mechanism that lets a website define which resources may load or execute and which security-sensitive behaviors are permitted. Its most important practical use is reducing the damage caused by cross-site scripting (XSS): even if malicious HTML reaches a page, a strong CSP can prevent the injected JavaScript from executing. CSP Level 3 is the current W3C specification line; as of September 2026, it remains a Working Draft, with the current W3C draft dated August 13, 2026. (w3.org)

CSP should not be treated as a replacement for proper escaping, sanitization, secure templating, or fixing XSS vulnerabilities. It is defense in depth: an additional enforcement layer provided by the browser. (web.dev)

What Is CSP?

A Content Security Policy is usually sent through the HTTP Content-Security-Policy response header:

Content-Security-Policy: default-src 'self'

This tells the browser that resources governed by default-src should normally come from the same origin as the page.

A policy can contain many directives, each controlling a different capability:

DirectiveControls
script-srcJavaScript execution and loading
style-srcCSS loading and inline styles
img-srcImages
connect-srcfetch(), XHR, WebSocket and similar connections
font-srcFonts
frame-srcFrames the page may load
worker-srcWorkers and service workers
object-srcPlugin/object resources
base-uriAllowed <base> URLs
form-actionForm submission destinations
frame-ancestorsSites allowed to embed the page

default-src acts as a fallback for many fetch directives. For example, if img-src is absent, images can fall back to default-src. This does not apply universally: frame-ancestors, form-action, and base-uri, for example, do not inherit from default-src. (w3.org)

Why CSP Matters

Consider a vulnerable page that accidentally renders attacker-controlled HTML:

<script>
  stealSession();
</script>

Without additional protection, the browser may execute it.

A strong CSP changes the question from:

“Is this valid JavaScript?”

to:

“Has this page explicitly authorized this JavaScript?”

That distinction is why CSP can materially reduce the impact of many XSS vulnerabilities.

The difficult part is deciding how scripts become trusted.

Older CSP configurations commonly relied on domain allowlists:

script-src 'self' https://cdn.example.com

This is better than allowing arbitrary scripts, but large host-based allowlists can become difficult to reason about and may contain origins that themselves provide ways to execute attacker-controlled code. Modern CSP guidance therefore favors nonce- or hash-based strict CSP for script execution. (developer.mozilla.org)

Strict CSP

A basic nonce-based strict policy can look like this:

Content-Security-Policy:
  script-src 'nonce-RANDOM_VALUE' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

The important part is the nonce.

The server generates a fresh unpredictable value for each response:

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

The same value appears in the CSP header:

script-src 'nonce-RANDOM_VALUE'

The browser executes the script because the values match. An injected script without the correct nonce is blocked.

For this design to provide meaningful protection, the nonce must be unpredictable and regenerated for every response. web.dev recommends a cryptographically strong value, ideally at least 128 bits. (web.dev)

Do not generate one nonce during application startup and reuse it indefinitely. A nonce that remains constant stops functioning as a meaningful one-time authorization token.

Nonces vs Hashes

Nonces and hashes solve similar problems but suit different architectures.

Nonces are usually easier for dynamically rendered HTML. The server generates a fresh value during each request and injects it into approved <script> elements.

Hashes work well for stable or statically generated HTML. Instead of generating a random value, you calculate a cryptographic hash of an authorized script:

script-src 'sha256-AbCdEf...'

The browser hashes the script and executes it only if the value matches the policy.

CSP supports SHA-256, SHA-384, and SHA-512 hash source expressions. Changing even a small part of an inline script changes its hash, so build systems using hash-based CSP generally need to regenerate the policy when script content changes. MDN also documents additional requirements when hashes are used to authorize external scripts, including the corresponding integrity attribute. (developer.mozilla.org)

For an SSR application, a nonce is often the simpler engineering choice. For static HTML where per-response mutation is undesirable, hashes may fit better.

What strict-dynamic Does

Modern applications frequently have a trusted bootstrap script that loads other scripts dynamically.

Without additional handling, those secondary scripts may be blocked.

Adding:

'strict-dynamic'

allows trust established by a valid nonce or hash to propagate to scripts loaded by that trusted script. (developer.mozilla.org)

For example:

script-src 'nonce-abc123' 'strict-dynamic'

and:

<script nonce="abc123" src="/bootstrap.js"></script>

permit /bootstrap.js to execute and can allow scripts that it subsequently creates and loads.

There is an important consequence: in browsers applying CSP Level 3 semantics, host expressions such as 'self', https: and explicit host allowlists in that script-src directive are ignored when 'strict-dynamic' is active with valid nonce/hash trust. That makes the trusted script chain itself an important security boundary. (developer.mozilla.org)

If trusted JavaScript dynamically constructs script URLs from attacker-controlled input, CSP cannot automatically repair that design.

A Practical Policy

A real application usually needs more than script-src.

For example:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{NONCE}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'none';
  form-action 'self';
  frame-ancestors 'none';

This should be treated as an architectural example, not a policy to paste blindly into every site.

frame-ancestors 'none', for example, prevents other pages from embedding the document and can help defend against UI-redressing attacks. But an application intentionally embedded by customers may instead require specific parent origins. The directive does not inherit from default-src, so it must be declared separately when required. (w3.org)

Similarly, connect-src must include APIs, WebSocket endpoints or other network destinations actually used by the application.

The goal is not to create the longest possible CSP. It is to describe the application's legitimate behavior as narrowly and accurately as practical.

Avoid unsafe-inline

A common response to CSP errors is:

script-src 'self' 'unsafe-inline'

That frequently defeats one of CSP's most valuable protections because arbitrary inline JavaScript may execute.

A better approach is to refactor patterns such as:

<button onclick="save()">Save</button>

into JavaScript event registration:

<button id="save">Save</button>

<script nonce="{NONCE}">
document
  .getElementById('save')
  .addEventListener('click', save);
</script>

Strict CSP also normally prevents JavaScript URLs such as href="javascript:..." and blocks string-to-code execution such as eval() unless 'unsafe-eval' is explicitly permitted. (web.dev)

Adding 'unsafe-inline' or 'unsafe-eval' simply to silence CSP errors should therefore trigger investigation rather than becoming the default fix.

Roll Out With Report-Only

Deploying a strict policy directly to production can break analytics, payment widgets, authentication flows, customer-support tools, API calls or application code that was never designed for CSP.

Use:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'nonce-{NONCE}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Report-only mode evaluates the policy without enforcing the blocks. Browser console messages and CSP reports reveal what the proposed policy would reject. (developer.mozilla.org)

A practical rollout becomes:

Observe → classify violations → fix legitimate dependencies → tighten the policy → enforce → continue monitoring.

Do not automatically add every reported hostname to an allowlist. A violation may reveal obsolete code, an unexpected third-party dependency, a browser extension, an attempted injection or a genuine configuration requirement.

CSP Reporting

Modern CSP reporting uses the Reporting API with a Reporting-Endpoints header and the CSP report-to directive:

Reporting-Endpoints:
  csp="https://example.com/csp-reports"

Content-Security-Policy:
  default-src 'self';
  report-to csp;

report-uri is deprecated by CSP Level 3 in favor of report-to. However, current MDN guidance still shows both where compatibility with browsers lacking complete report-to support is required. (w3.org)

Reports should also be treated as untrusted input. Logging infrastructure should validate, rate-limit and safely store them rather than assuming every report is trustworthy.

Header vs <meta>

CSP can also be delivered through:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'"
>

but an HTTP header is generally preferable for production.

Meta-delivered CSP has important limitations. It cannot provide report-only policies, and CSP Level 3 specifies that directives including frame-ancestors, report-uri, and sandbox are not supported through the meta mechanism. A meta policy also cannot retroactively control resources processed before the browser encounters it. (w3.org)

Common CSP Mistakes

The most common engineering mistakes are not syntax errors but trust-model errors: treating a huge hostname list as strong protection, reusing nonce values, automatically adding domains after every violation, enabling 'unsafe-inline' to make errors disappear, or assuming default-src 'none' also configures directives such as frame-ancestors and form-action.

Another subtle mistake is delivering multiple CSP policies and expecting the second one to loosen the first. Multiple enforced policies are applied together: a request must satisfy all applicable policies. A second header cannot simply override an earlier restrictive policy. (w3.org)

SeoNest Recommendation

Start from the application's real resource graph rather than copying a generic CSP generator result.

For dynamically rendered applications, prefer a properly generated per-response nonce and a strict script-src. For static applications, investigate a hash-based policy. Explicitly define security-sensitive directives such as object-src, base-uri, form-action, and frame-ancestors according to the product's actual requirements.

Deploy the proposed policy in report-only mode first, investigate violations, remove incompatible JavaScript patterns, then enable enforcement.

Most importantly, keep CSP in the correct place in the security model: it is a browser-enforced containment mechanism, not permission to leave XSS vulnerabilities unfixed.

FAQ

Does CSP prevent all XSS?

No. CSP can substantially restrict script execution and reduce the impact of many XSS vulnerabilities, but bypasses remain possible when trusted scripts themselves expose exploitable behavior. Input handling, context-aware output encoding, sanitization and secure application design remain necessary. (web.dev)

Should every website use default-src 'none'?

Not automatically. It is a strong starting posture because resources must then be deliberately permitted, but the resulting directives still need to match the application's architecture. Some directives do not inherit from default-src.

Should I use a nonce or a hash?

Use a nonce when the server can generate and modify HTML for every response. Hashes are often more practical for stable static HTML. (developer.mozilla.org)

Can CSP be configured in Nginx?

Yes. Nginx can send CSP headers, but nonce-based CSP usually requires coordination with the application or rendering layer because a fresh nonce must appear in both the response header and authorized HTML.

How do I know whether CSP works?

Inspect the actual response headers, test expected application flows, watch browser DevTools for CSP violations, collect reports during report-only deployment, deliberately test prohibited resources, and repeat those tests after enforcement.

Final Takeaway

A useful CSP is not merely a list of trusted domains. It is an explicit browser-enforced trust model.

The strongest practical approach for many modern applications is a strict policy based on cryptographic nonces or hashes, supported by directives that constrain framing, form submissions, plugins and other resources. Deploy it gradually, measure violations before enforcement, and treat every exception as an architectural decision rather than another hostname to append.

Sources

  1. W3C — Content Security Policy Level 3, Working Draft, August 13, 2026. (w3.org) W3C CSP Level 3 specification
  2. MDN Web Docs — Content Security Policy (CSP), accessed September 19, 2026. (developer.mozilla.org) MDN CSP Guide
  3. MDN Web Docs — Content-Security-Policy header, accessed September 19, 2026. (developer.mozilla.org) MDN CSP Header Reference
  4. web.dev — Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP), accessed September 19, 2026. (web.dev) web.dev Strict CSP Guide
  5. MDN Web Docs — Content Security Policy implementation, accessed September 19, 2026. (developer.mozilla.org) MDN CSP Implementation Guide
  6. MDN Web Docs — frame-ancestors directive, accessed September 19, 2026. (developer.mozilla.org) MDN frame-ancestors Reference
  7. MDN Web Docs — Content-Security-Policy-Report-Only, accessed September 19, 2026. (developer.mozilla.org) MDN CSP Report-Only Reference

SEONEST

Need a stronger technical foundation?

We build production-ready websites where SEO, speed and clean engineering are part of the architecture from the start.

Discuss your project