Content Security Policy in Practice: CSP for JavaScript Applications
AI generated
JS
() =>
JavaScript · Web Security · HTTP Headers
Content Security Policy in Practice
from the first directive to enforcement

A Content Security Policy is the single most effective HTTP header against cross site scripting, yet most projects either skip it entirely or ship it with a far too generous whitelist. This article shows how a Content Security Policy grows from the first directive through nonces and hashes all the way to a hard enforcement mode, without suddenly breaking inline scripts or third party tools.

18 min read script-src · nonce · strict-dynamic · reporting Chrome · Firefox · Safari · Edge

1. What Content Security Policy really does

Anyone introducing a Content Security Policy for the first time frequently underestimates how many existing code paths have implicitly benefited from the absence of enforcement. That is exactly why a gradual rollout pays off, rather than switching the policy fully live in a single day.

A Content Security Policy is an HTTP response header through which an application tells the browser which sources are allowed for scripts, styles, images and other resources. Unlike a web application firewall ruleset, a Content Security Policy acts directly in the user's browser, after the page has already been delivered. That makes it the last line of defense against cross site scripting, even when an injection vulnerability in the backend has gone undetected.

The practical benefit shows up clearly in one concrete scenario: an attacker smuggles a script tag into a page through an unfiltered form field. Without a Content Security Policy, the browser executes that script without complaint and sends session cookies to a foreign domain. With a correctly configured Content Security Policy, the browser refuses to run it, because the injected script matches no allowed source, no valid nonce and no matching hash. That exact difference is what makes a Content Security Policy one of the most effective building blocks of modern JavaScript security.

2. The most important CSP directives in detail

The central directive of every Content Security Policy is script-src, which defines which sources JavaScript may be executed from. If script-src is missing, default-src acts as a fallback, which in practice is rarely precise enough. Alongside it, style-src governs stylesheets and inline styles, img-src governs image sources, connect-src governs targets for fetch, XHR and WebSocket connections, and frame-ancestors replaces the outdated X Frame Options header for controlling embedding in iframes.

An often underestimated directive is object-src 'none', which completely blocks plugins like Flash or outdated Java applets and should be set in practically every modern Content Security Policy. base-uri 'self' prevents an attacker from manipulating the base element and thereby redirecting relative URLs to a foreign domain. Combining these directives forms the basic framework before nonces or hashes even enter the picture.


Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.mironsoft.de;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https://images.mironsoft.de;
  connect-src 'self' https://api.mironsoft.de;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';
  upgrade-insecure-requests;

This baseline configuration already blocks a large share of naive injection attempts, but leaves room for improvement: 'unsafe-inline' on style-src is a compromise that should later be replaced with nonces or hashes once the project is ready for it.

3. Nonce based CSP for server rendered scripts

A nonce is a random value, freshly generated per request, that must appear both in the Content-Security-Policy header and in the nonce attribute of every allowed script tag. Only scripts carrying the correct nonce are executed by the browser, every other inline script, including ones injected by an attacker, is discarded. The decisive advantage over a plain domain whitelist: an attacker cannot guess the nonce, because it is regenerated per response and must never be reused.

In server rendered applications, for example Magento templates or a Node based rendering layer, the nonce is generated centrally in the request context and injected into both the header and every template. It is important that the nonce is never computed in client side JavaScript, because then an attacker could read it straight out of the source. The nonce must come from a cryptographically secure random source, carry at least 128 bits of entropy, and be regenerated on every single page load.


// Node/Express middleware: generate a per-request nonce
const crypto = require('crypto');

app.use((req, res, next) => {
  // 16 random bytes, base64 encoded, unique per response
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${res.locals.cspNonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self';`
  );
  next();
});

// In the template engine, print the nonce on every allowed script tag
// <script nonce="<%= cspNonce %>">/* application bootstrap */</script>

4. Hash based CSP for static inline scripts

When inline scripts are fixed in the source code and do not change between deployments, a hash is often more practical than a nonce. The Content Security Policy then only allows scripts whose SHA256, SHA384 or SHA512 hash exactly matches the value stated in the header. If even a single character in the script content changes, whitespace or a comment for instance, the hash changes, and the Content Security Policy blocks the script on the next load.

This sensitivity is both the strength and the weakness of the hash approach: it reliably prevents a tampered script from slipping through unnoticed, but it requires a build step that automatically recomputes hashes and writes them into the header or meta tags. For small, stable inline scripts such as an analytics bootstrap snippet or a feature detection script, the hash approach is the more robust choice compared to a nonce, because no server context is needed to generate it.


# Compute the SHA256 hash of an inline script for CSP hash-source
# The hash must cover exactly the text node content between <script> tags
echo -n "console.log('inline bootstrap');" | \
  openssl dgst -sha256 -binary | openssl base64

# Resulting header entry:
# script-src 'self' 'sha256-<base64-hash-here>';

# Multiple hashes can be listed for several distinct inline scripts
# script-src 'self' 'sha256-<hash-one>' 'sha256-<hash-two>';

A build script that automatically iterates over every inline script on each deployment and recomputes current hashes prevents a forgotten hash update from disabling the Content Security Policy after the next code change. This automation matters especially once several teams work concurrently on different inline scripts of the same page.

5. strict-dynamic and retiring domain whitelists

Domain whitelists like script-src https://a.example.com https://b.example.com are considered an anti pattern today, because practically every large domain has an open redirect or JSONP gap somewhere that an attacker can abuse as a stepping stone. The keyword 'strict-dynamic' solves this by propagating trust along the execution chain: a script loaded with a valid nonce or hash is itself allowed to load further scripts via document.createElement('script'), without those needing to be explicitly listed again.

The clever part of 'strict-dynamic' is that classic domain entries are then ignored by modern browsers, which ensures backward compatibility with older browsers through a duplicate entry: script-src 'nonce-xyz' 'strict-dynamic' https://fallback.example.com. Modern browsers only honor the nonce and 'strict-dynamic', older browsers without support fall back to the domain list. This combination is now considered the reference implementation used by Google, GitHub and other large platforms.

For Magento and Hyvä based frontends, introducing 'strict-dynamic' concretely means that layout XML generated inline scripts must first receive a server side generated nonce before the Content Security Policy can take effect at all. Only after that can the domain whitelist be gradually reduced without endangering existing third party integrations in the checkout.

6. Report only mode and violation reporting

Before a Content Security Policy is switched live into enforcement mode, it should be observed through the Content-Security-Policy-Report-Only header. In this mode the browser blocks nothing, but reports every violation to a configured endpoint. That way, forgotten inline scripts, dynamically loaded third party widgets and unknown analytics snippets can be identified before the enforced policy breaks them in live operation.

The modern Reporting API replaces the older report-uri format with report-to combined with a separate Reporting-Endpoints header that delivers structured JSON reports. Each report contains the blocked URI, the violated directive and the line in the document. In practice it pays off to collect reports for several weeks in report only mode before switching to enforcement mode, because rare code paths are otherwise easily overlooked.


Reporting-Endpoints: csp-endpoint="https://mironsoft.de/csp-reports"
Content-Security-Policy-Report-Only:
  script-src 'self' 'nonce-abc123' 'strict-dynamic';
  report-to csp-endpoint;

7. Common mistakes when introducing CSP

The most common mistake is reaching blindly for 'unsafe-inline' on script-src, which destroys the entire protective effect of the Content Security Policy against cross site scripting. Once 'unsafe-inline' is set, any injected inline script can be executed, regardless of nonce or hash. A second classic mistake is testing the policy only in one's own browser, without considering Safari and older Edge versions, which interpret directives such as strict-dynamic with differing strictness.

A third mistake concerns inline event handlers like onclick="doSomething()" in HTML attributes: these are also treated as inline scripts by script-src and get blocked without a nonce, which causes unexpected breakage in legacy codebases. The fix is a consistent migration to addEventListener, which is the modern JavaScript style anyway. Introducing the Content Security Policy step by step through report only mode surfaces these spots before the production rollout.

8. CSP and third party scripts together

Third party scripts such as tag managers, payment widgets or chat systems are the biggest practical hurdle when introducing a strict Content Security Policy, because they frequently load further scripts themselves, set inline styles, or embed their own iframes. Every one of these behaviors must be explicitly accounted for in the policy, otherwise the third party script silently stops working correctly without an obvious error message.

A proven approach is to observe each third party script individually in report only mode and document its actual requirements, rather than preemptively adopting a large domain list. Many tag manager vendors now support 'strict-dynamic' compatible loading paths themselves, so a single nonce on the initial tag manager script suffices to cover the entire chain of subsequently loaded scripts. This compatibility should be explicitly clarified with the vendor before going live.

9. CSP strategies compared

The three common strategies, domain whitelist, nonce and hash, differ significantly in maintenance effort, security level and suitability for particular script types. The following table summarizes the practical differences.

Strategy Security level Maintenance effort Suited for
Domain whitelist Low Low, but gaps via open redirects Legacy projects without a build step
Nonce based High Server must generate a nonce per request Dynamically rendered pages
Hash based High Build step needed to recompute hashes Static, unchanging inline scripts
strict-dynamic + nonce Very high One time setup, then low Modern applications with dynamic loading

In practice the path almost always runs through a combination: first a domain whitelist in report only mode, then a gradual migration to nonces for dynamic content and hashes for stable inline scripts, until 'strict-dynamic' finally renders the last whitelist entries obsolete. This migration can be spread over weeks without endangering live operation.

Mironsoft

JavaScript security, CSP rollout and frontend hardening

A Content Security Policy that actually protects, without blocking your application?

We analyze existing applications, design a fitting Content Security Policy and accompany the migration from report only to hard enforcement mode, including third party integration.

CSP Audit

Analysis of existing scripts, styles and third party integrations

Nonce integration

Server side nonce generation for templates and rendering layers

Reporting setup

Violation reporting endpoint and monitoring for ongoing operation

10. Summary

A Content Security Policy is not a header you set once, but an iterative process from an initial domain whitelist through nonces and hashes to a 'strict-dynamic' enforcement mode. Report only mode is the single most important tool for surfacing violations before they disrupt live operation. Nonces suit dynamically rendered scripts, hashes suit stable inline content, and 'strict-dynamic' solves the problem of subsequently loaded scripts without having to maintain every domain individually.

The biggest lever is reviewing third party scripts individually and early, and treating the Content Security Policy not as a one time security project but as a permanent part of the deployment. Every new integration, every new analytics tool and every new widget must be tested against the existing policy before going live. That discipline is what separates a policy that exists on paper from one that actually protects against cross site scripting.

Content Security Policy in Practice — The Essentials at a Glance

Directives

script-src, object-src 'none' and base-uri 'self' form the foundation of every Content Security Policy.

Nonce vs. hash

Nonce for dynamically rendered scripts, hash for stable inline content that has no build dependency on the request context.

strict-dynamic

Allows loading further scripts along a trusted chain, replacing fragile domain whitelists.

Rollout

Always start with report only mode and a reporting endpoint, only then switch to enforcement mode.

11. FAQ: Content Security Policy in Practice

1What is a Content Security Policy?
An HTTP header defining allowed sources for scripts and resources, acting in the browser as the last line of defense against cross site scripting.
2Why is unsafe-inline dangerous?
It allows every inline script without checks and defeats the protective effect against cross site scripting.
3Nonce or hash?
Nonce for dynamically rendered pages, hash for static, unchanging inline scripts.
4What does strict-dynamic do?
Propagates trust along the execution chain so loaded scripts may load further scripts.
5How do I safely test a new policy?
With Content-Security-Policy-Report-Only: blocks nothing but reports every violation to an endpoint.
6Does CSP block onclick attributes?
Yes, without a nonce inline event handlers are blocked. Migrating to addEventListener is the fix.
7How do I handle third party scripts?
Observe each script individually in report only mode instead of preemptively adopting a large whitelist.
8Does frame-ancestors replace X-Frame-Options?
Yes, with finer grained control over allowed iframe embedding.
9How often should a nonce be regenerated?
On every page load, from a cryptographically secure random source with at least 128 bits of entropy.
10How long should the report only phase last?
Several weeks, so rare code paths and third party integrations are captured before enforcement.