Content Security Policy (CSP): The Immune System of the Web
Preventing XSS and Magecart attacks. A definitive guide to implementing Strict CSP with Nonces, Report-Only mode, and 'strict-dynamic' in Remix/Hydrogen.
In 2018, British Airways suffered a catastrophic data breach. Hackers stole the credit card details of 380,000 customers.
They didn’t break into the database. They didn’t guess the admin password.
They injected 22 lines of JavaScript into the checkout page via a compromised third-party library. This script silently read form inputs and sent them to baways.com (a fake domain).
This is a Supply Chain Attack (specifically Formjacking or Magecart). The scary part? Your firewall (WAF) cannot stop it. The request comes from the user’s browser, not your server.
The only defense against this is Content Security Policy (CSP). CSP is an HTTP Header that tells the browser: “These are the trusted domains. Block everything else.”
At Maison Code Paris, we consider CSP mandatory for any site executing transactions. Operating without it is like leaving the vault door open because “the lock is hard to use.”
Why Maison Code Discusses This
Security is not an add-on; it is our baseline. For high-value specialized retail, the risk of Magecart (Digital Skimming) is existential. We recently audited a prospective client and found a malicious jQuery plugin harvesting credit card inputs. It had been there for 6 months. A Strict CSP would have blocked this instantly. We implement Nonce-based CSP on every Headless storefront we ship, ensuring that only code we explicitly trust can execute in your customer’s browser.
The Mechanism: Whitelisting
By default, existing web browsers are promiscuous. If the HTML says <script src="https://evil.com/hack.js">, the browser loads it.
CSP changes this default to “Deny All”.
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.google.com;
With this header, if an attacker injects <script src="https://evil.com/hack.js">, the browser console screams in red: Refused to load script because it violates the following Content Security Policy directive. The script never executes.
The Strategy: Nonce-based CSP (Strict CSP)
Whitelisting domains (https://google.com) is fragile. Google hosts millions of scripts (Drive, Maps, User Content). If an attacker can upload a file to Google Drive, they can bypass your whitelist.
The Gold Standard is Nonce-based CSP. A Nonce (Number used ONCE) is a random cryptographic token generated by the server for every single request.
- Server Generation:
const nonce = crypto.randomBytes(16).toString('base64'); // "r4nd0m" - Header:
script-src 'nonce-r4nd0m' - HTML Injection:
<script nonce="r4nd0m" src="/app.js"></script>
If an attacker injects <script>alert(1)</script>, they don’t know the nonce. The browser blocks it.
Implementing in Hydrogen (Remix)
In a Server-Side Rendered (SSR) app like Hydrogen, we generate the nonce in the entry.server.tsx.
// app/entry.server.tsx
import { createContentSecurityPolicy } from '@shopify/hydrogen';
export default async function handleRequest(request, responseStatusCode, responseHeaders, remixContext) {
// Hydrogen's helper generates the nonce and the header
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
// Standard Directives
defaultSrc: ["'self'"],
imgSrc: ["'self'", 'cdn.shopify.com', 'data:', 'https://www.google-analytics.com'],
styleSrc: ["'self'", "'unsafe-inline'"], // Valid for Styled Components
connectSrc: ["'self'", 'https://monorail-edge.shopifysvc.com', 'https://vitals.vercel-insights.com'],
// The Critical Part
scriptSrc: [
"'self'",
// 'strict-dynamic' tells modern browsers: "Trust any script loaded by a trusted script"
// This allows GTM to load Facebook Pixel without us whitelisting Facebook explicitly.
"'strict-dynamic'",
// Fallback for older browsers
'https://cdn.shopify.com',
'https://www.googletagmanager.com'
],
});
responseHeaders.set('Content-Security-Policy', header);
return (
// NonceProvider passes the nonce to the <Scripts /> component
<NonceProvider>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>
);
}
The “Strict Dynamic” Directive
Modern sites use Google Tag Manager (GTM).
GTM loads Facebook Pixel. Facebook Pixel loads other tracking scripts.
You cannot predict the full tree of dependencies.
This led to developers enabling 'unsafe-eval' and 'unsafe-inline', which defeats the purpose of CSP.
strict-dynamic solves this.
It says: “If a script is trusted (has a valid nonce), allow it to load other scripts dynamically.”
Since we trust GTM (by noncing it), we trust what GTM loads.
Warning: This puts a lot of trust in GTM. Ensure your GTM container has strict access controls.
Identifying Directives
A robust policy covers more than scripts.
base-uri 'none': Prevents<base>tag hijacking (redirecting relative links to evil sites).object-src 'none': Blocks Flash, Java applets, and<embed>.frame-ancestors 'none': Prevents Clickjacking. Your site cannot be put in an Iframe.form-action 'self': Ensures forms can only submit to your own API. Prevents attackers from changing a form action to their server.connect-src: Restrictsfetch()/XHR. Even if an attacker executes JS, they can’t send the data toevil.com.
deploying Safely: The Report-Only Mode
You cannot “Turn On” CSP on Friday afternoon. You will break the site. You will block the Chat Widget. You will block the Reviews App. You start in Report Only Mode.
Content-Security-Policy-Report-Only: default-src 'self'; ... report-uri /api/csp-report;
The browser will execute everything, but it will send a JSON violation report to your backend whenever it would have blocked something.
The Collector Endpoint
You need a service to ingest these reports. Using Sentry or Datadog is best; they visualize the violations.
{
"csp-report": {
"document-uri": "https://maisoncode.paris/checkout",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'nonce-xyz'",
"blocked-uri": "https://malicious-analytics.com/tracker.js",
"status-code": 200
}
}
Workflow:
- Deploy
Report-Only. - Wait 1 week.
- Analyze logs. “Ah, we forgot to whitelist the Pinterest Tag.”
- Update Policy.
- When logs are quiet (only actual attacks), switch to
Content-Security-Policy(Enforce).
Refactoring Code for CSP
CSP forces you to write better code. It blocks Inline Event Handlers.
Bad (Blocked):
<button onclick="trackClick()">Buy</button>
Good (Allowed):
<button id="buyBtn">Buy</button>
<script nonce="...">document.getElementById('buyBtn').addEventListener('click', trackClick);</script>
It separates behavior from markup.
10. Trusted Types (The Future of XSS Prevention)
CSP blocks the source of the script.
Trusted Types blocks the sink (innerHTML).
It forces developers to sanitize strings before injecting them.
div.innerHTML = string -> Blocked.
div.innerHTML = TrustedHTML.create(string) -> Allowed.
This eliminates DOM XSS vulnerabilities at the browser engine level.
It is strict (“Hard Mode”), but for banking-grade security, it is the endgame.
11. Edge CSP (Cloudflare Workers)
Generating CSP at the origin (Node.js) is good. Injecting it at the Edge is better. We use Cloudflare Workers to inject security headers. This protects static assets (images, bare HTML files) that might not pass through the Node.js application logic. It also allows “Emergency Blocking”. If a library is compromised, we can update the CSP at the Edge in 300ms globally.
13. CSP in a Micro-Frontend World
What if you load a widget from Team B?
You cannot share the Nonce (it’s generated on the server).
Hash-Based CSP.
Instead of noncing, you calculate the SHA-256 hash of the script content.
script-src 'sha256-K7g...'.
The browser hashes the inline script. If it matches, it runs.
- Pro: Static. Can be cached.
- Con: If you change one character (even a space), the hash breaks. We use this for stable, 3rd party vendor scripts that rarely change version.
14. The “Unsafe-Hashes” Keyword
Sometimes you must use inline event handlers (legacy libraries).
navigate('foo') inside an onclick.
Standard CSP blocks this.
'unsafe-hashes' allows you to whitelist specific event handler strings.
script-src 'unsafe-hashes' 'sha256-...' (where hash is of “navigate(‘foo’)”).
This allows you to lock down legacy code without rewriting the entire DOM event model.
15. Conclusion
Content Security Policy is the seatbelt of the web. It doesn’t prevent the crash (injection), but it prevents you from flying through the windshield (data exfiltration). It is tedious to configure. It requires constant maintenance as you add new marketing tools. But for a luxury brand dealing with high-net-worth individuals, it is the baseline of trust.
Is your site wide open?
If you don’t send a CSP header, you are vulnerable to Magecart.