Why Payment Pages Are Target Number One
Magecart is not a single group — it is an umbrella term for dozens of threat actors who specialize in injecting card-skimming JavaScript into checkout pages. The attack is elegant in its simplicity: get a malicious <script> tag onto a payment page, capture every card number entered into the form, and exfiltrate the data to an attacker-controlled endpoint. British Airways, Ticketmaster, Newegg — the list of high-profile victims keeps growing.
The reason payment pages are so attractive is concentration of value. A single compromised checkout can harvest thousands of card numbers per day. Unlike server-side breaches that require deep network access, a Magecart attack only needs one injection point: a compromised third-party script, a stored XSS vulnerability, or a supply-chain attack on an npm package.
Content Security Policy is the single most effective browser-side defense against these attacks. It tells the browser exactly which scripts, styles, frames, and connections are allowed on a page. Anything not on the allowlist gets blocked — and optionally reported back to you.
How CSP Blocks Script Injection
Here is the flow when a Magecart-style script tries to execute on a page protected by CSP:
The legitimate Stripe.js script loads fine because it matches our allowlist. The injected skimmer from evil-cdn.com gets killed instantly. The browser also fires a violation report back to our reporting endpoint, so we know someone tried.
CSP Directives That Matter for Payment Pages
A full CSP policy has over a dozen directives, but for payment pages, these are the ones that will save you:
script-src— Controls which JavaScript can execute. This is your primary defense against Magecart. Every third-party payment SDK needs to be explicitly allowed here.connect-src— Restricts wherefetch(),XMLHttpRequest, and WebSocket connections can go. Blocks data exfiltration to attacker-controlled servers.frame-src— Controls which origins can be embedded in iframes. Payment providers like Stripe and Adyen render card input fields inside iframes for PCI isolation.style-src— Governs CSS loading. Payment SDKs often inject inline styles for their UI components.img-src— Controls image loading. Card brand logos and 3D Secure challenge images need to load from specific origins.default-src— The fallback for any directive you do not explicitly set. Always start with'none'or'self'and build up from there.
Tip: Start with default-src 'none' and add permissions one directive at a time. It is far easier to add what you need than to figure out what to remove from an overly permissive policy.
The Hard Part: Third-Party Payment SDKs
Here is where most teams get stuck. You cannot just set script-src 'self' and call it a day. Stripe.js loads from js.stripe.com. Adyen's Web Components pull from checkoutshopper-live.adyen.com. PayPal's SDK comes from multiple subdomains. Each provider needs specific origins whitelisted across multiple directives.
When we first deployed CSP on our checkout, we broke Stripe Elements immediately. The card input iframe refused to render because we had not added js.stripe.com to frame-src. The 3D Secure challenge popup failed because we missed hooks.stripe.com in frame-src. It took us three rounds of testing to get the full allowlist right.
Here is the CSP header we ended up with for a checkout page using Stripe Elements:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://js.stripe.com 'nonce-{SERVER_GENERATED}';
style-src 'self' 'nonce-{SERVER_GENERATED}';
frame-src https://js.stripe.com https://hooks.stripe.com;
connect-src 'self' https://api.stripe.com https://maps.googleapis.com;
img-src 'self' https://*.stripe.com data:;
font-src 'self';
base-uri 'self';
form-action 'self';
Every origin in that policy is there for a reason. js.stripe.com in script-src loads the Stripe.js library. The same origin in frame-src allows the card input iframe. hooks.stripe.com handles 3D Secure authentication flows. api.stripe.com in connect-src is where the tokenization API calls go.
Nonce-Based vs. Hash-Based Script Policies
There are three main strategies for allowing scripts in CSP. The security tradeoffs are significant:
| Strategy | How It Works | Security Level | Maintenance |
|---|---|---|---|
'unsafe-inline' |
Allows all inline scripts | Low — defeats CSP purpose | None |
| Hash-based | SHA-256 hash of each inline script | Medium — static scripts only | Update hash on every code change |
| Nonce-based | Server-generated random token per request | High — recommended approach | Server must generate nonce per request |
'strict-dynamic' |
Trust propagates from nonced script to its children | High — best for complex pages | Requires nonce + modern browsers |
We went with nonce-based CSP. Every page render generates a cryptographically random nonce on the server side. That nonce gets injected into both the CSP header and the <script> tags. An attacker who manages to inject a script tag cannot guess the nonce — it changes on every single request.
Here is what the server-side implementation looks like in a Node.js/Express middleware:
const crypto = require('crypto');
app.use((req, res, next) => {
// Generate a random 128-bit nonce for this request
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy',
`default-src 'none'; ` +
`script-src 'self' https://js.stripe.com 'nonce-${nonce}'; ` +
`style-src 'self' 'nonce-${nonce}'; ` +
`frame-src https://js.stripe.com https://hooks.stripe.com; ` +
`connect-src 'self' https://api.stripe.com; ` +
`img-src 'self' https://*.stripe.com data:; ` +
`font-src 'self'; ` +
`base-uri 'self'; ` +
`form-action 'self'; ` +
`report-uri /csp-report`
);
next();
});
Then in your HTML template, every inline script gets the nonce attribute:
<script nonce="<%= cspNonce %>">
// This script will execute — it has the correct nonce
Stripe('pk_live_your_key').elements().create('card');
</script>
Report-Only Mode: Deploy Without Fear
This is the single most important lesson we learned: never go straight to enforcement. CSP has a built-in safety net called Content-Security-Policy-Report-Only. It evaluates the policy and reports violations, but does not actually block anything.
We ran in Report-Only mode for three weeks before flipping to enforcement. In that window, we caught 14 violations we had not anticipated — analytics scripts loaded by our marketing team, a font loaded from an unexpected CDN, and a legacy inline event handler buried in a partial template nobody had touched in two years.
The switch from Report-Only to enforcement is a one-line change — you just rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. But those three weeks of data collection saved us from a checkout outage that would have cost real revenue.
Building a CSP Reporting Pipeline
A CSP policy without reporting is flying blind. When the browser blocks a script, you want to know about it — both to catch real attacks and to detect legitimate scripts you forgot to allowlist.
There are two reporting mechanisms. The older report-uri directive sends a JSON POST to a URL you specify. The newer report-to directive uses the Reporting API and is more flexible but has less browser support as of early 2026. We use both for maximum coverage:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://js.stripe.com 'nonce-abc123';
...
report-uri /csp-report;
report-to csp-endpoint;
// Reporting-Endpoints header (for report-to)
Reporting-Endpoints: csp-endpoint="/csp-report"
On the backend, our /csp-report endpoint parses the violation JSON, deduplicates by source file and violated directive, and pushes to our existing alerting pipeline. We set up two alert tiers: a Slack notification for any new violation source we have not seen before, and a PagerDuty alert if we see more than 50 violations from the same unknown origin within five minutes — that pattern strongly suggests an active injection attack.
In the first month of enforcement, our reporting pipeline caught 3,200 violation reports. The vast majority were browser extensions injecting scripts — harmless, but noisy. We filtered those out by ignoring reports where source-file started with chrome-extension:// or moz-extension://. The remaining reports were all legitimate: two were our own developers testing with inline scripts locally, and one was a CDN migration that had not been updated in the CSP yet.
Common Mistakes That Undermine Your CSP
After reviewing CSP implementations across a dozen FinTech codebases (including our own early attempts), these are the mistakes I see most often:
1. Using 'unsafe-inline' in script-src
This single directive negates almost all of CSP's protection against XSS. If you allow 'unsafe-inline', an attacker who finds an injection point can run arbitrary JavaScript. I have seen teams add it "temporarily" to fix a broken integration and never remove it. If you need inline scripts, use nonces.
2. Wildcard subdomains on CDN origins
A policy like script-src *.cloudfront.net is dangerously broad. Anyone can create a CloudFront distribution and host malicious scripts on it. Always use the specific subdomain: d1234abcdef.cloudfront.net.
3. Forgetting connect-src
Teams focus on script-src and forget that a skimmer needs to exfiltrate data. A tight connect-src directive is your second line of defense — even if a malicious script somehow executes, it cannot send the stolen card data anywhere if outbound connections are locked down.
4. Not testing 3D Secure flows
The main checkout might work fine, but 3D Secure authentication opens iframes to issuer bank domains. These vary by card issuer and are hard to predict. We solved this by using frame-src https://js.stripe.com https://hooks.stripe.com and letting Stripe handle the 3DS iframe chain internally.
PCI DSS 4.0 and Requirement 6.4.3
PCI DSS v4.0 — Requirement 6.4.3: All payment page scripts that are loaded and executed in the consumer's browser are managed as follows: a method is implemented to confirm that each script is authorized, the integrity of each script is assured, and an inventory of all scripts is maintained with written justification for why each is necessary. This requirement becomes mandatory on March 31, 2025 and is now fully enforceable. If your checkout pages do not have script management controls in place, you are out of compliance.
This is where CSP goes from "nice to have" to "compliance requirement." PCI DSS v4.0 requirement 6.4.3 explicitly mandates that merchants manage and authorize every script running on payment pages. CSP is not the only way to satisfy this requirement — Subresource Integrity (SRI) hashes and commercial script monitoring tools also count — but CSP is the most practical foundation.
Our approach to satisfying 6.4.3 was threefold: a strict CSP policy as the enforcement mechanism, SRI hashes on all third-party script tags as an integrity check, and a documented script inventory spreadsheet that maps every allowed origin to a business justification and an owner. During our last QSA audit, the assessor specifically called out our CSP reporting pipeline as a strong compensating control. It gave us real-time visibility into unauthorized script attempts — exactly what the requirement is asking for.
Practical tip: Keep your script inventory as a version-controlled file in your repo, not a shared Google Sheet. When someone adds a new third-party script, the pull request that updates the CSP policy should also update the inventory. Code review becomes your authorization workflow.
What Happened When It Mattered
Six weeks after we enforced CSP on our checkout pages, our reporting pipeline lit up. We saw a burst of 340 violation reports in under two minutes, all from the same source: a script trying to load from a domain that looked like a misspelled version of one of our legitimate CDN origins. The script-src directive blocked every single attempt. No card data was exposed. No customer was affected.
We traced it back to a compromised tag manager snippet on a marketing landing page that shared a template partial with the checkout. The attacker had injected a loader script into the tag manager configuration. Without CSP, that script would have executed silently on our payment page, capturing card numbers for who knows how long. With CSP, it was dead on arrival, and we had a full forensic trail from the violation reports.
That incident turned CSP from "that security header the compliance team wanted" into something every engineer on the team understood the value of. It is one thing to read about Magecart attacks in a blog post. It is another thing entirely to watch your CSP policy block one in real time.
References
- MDN Web Docs — Content Security Policy (CSP)
- Google Web Fundamentals — Content Security Policy
- PCI DSS v4.0 Standard — PCI Security Standards Council
- W3C — Content Security Policy Level 3 Specification
- Stripe Documentation — Content Security Policy Guide
Disclaimer: This article reflects the author's personal experience and opinions. It is not legal, compliance, or security advice. CSP implementation requirements vary depending on your payment provider, architecture, and compliance obligations. Always test thoroughly in Report-Only mode before enforcing, and consult with your QSA for PCI DSS compliance guidance. Product names and brands mentioned are property of their respective owners.