Adyen is one of those integrations where CSP gets real fast.
A normal marketing site can get away with a basic policy and a couple of allowlists. Payments are different. You’re loading third-party scripts, embedding frames, sending XHR requests to payment endpoints, and sometimes dealing with redirects or 3D Secure flows. If your CSP is too strict, checkout breaks. Too loose, and you’ve basically given up the point of having CSP.
I’ve had the best results by treating payment pages as their own security boundary. Don’t try to reuse the exact same CSP from your homepage or blog. Build a payment-specific policy.
What Adyen needs from CSP
For a typical Adyen Web integration, CSP usually needs to allow:
- Adyen JavaScript
- Adyen styles
- Adyen frames for card fields and challenge flows
- Adyen API calls
- Images and assets used by Adyen UI
- Your own backend endpoints for
/payments,/payments/details, or sessions
The exact domains can vary by:
- test vs live
- region
- Drop-in vs Components
- features like Apple Pay, Google Pay, PayPal, 3D Secure
That’s why hardcoding a giant permissive policy is a bad habit. Start with the Adyen features you actually use, then expand only when the browser reports a violation.
For official references, use Adyen’s documentation and browser CSP references from official docs. If you want a ready-to-use baseline pattern, https://csp-examples.com is useful for policy structure.
Start from a sane baseline
Here’s the real CSP header from headertest.com that you provided:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YmZkODljYWItY2ZiNC00Y2U2LTg4MjEtY2VhOGY1NDA2N2Y1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
That’s a decent marketing-site CSP, but it will not support Adyen checkout by itself. Missing pieces usually include:
- Adyen script origins in
script-src - Adyen stylesheet origins in
style-src - Adyen API endpoints in
connect-src - Adyen hosted frames in
frame-src - sometimes broader
img-srcfor payment logos or challenge assets
A practical Adyen CSP
Here’s a good starting point for an Adyen payment page. I’m keeping it focused and reasonably strict.
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
form-action 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;
style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
img-src 'self' data: https:;
font-src 'self' https://checkoutshopper-test.adyen.com;
connect-src 'self' https://checkoutshopper-test.adyen.com https://{your-api-origin};
frame-src 'self' https://checkoutshopper-test.adyen.com;
report-to default-endpoint;
report-uri /csp-report;
For live, you’ll replace the test host with the live Adyen host used by your account and region.
A few opinions here:
object-src 'none'should always be there.base-uri 'self'is cheap protection. Use it.frame-ancestors 'none'is right for standalone checkout pages unless your app must be embedded.- I’m fine with
'unsafe-inline'instyle-srcon payment pages if the library forces the issue. I’m not fine with it inscript-src. - Use nonces for your own inline bootstrapping code instead of allowing inline scripts globally.
Example HTML with nonce-based bootstrapping
This is the pattern I’d use for Adyen Web Drop-in.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Checkout</title>
<link rel="stylesheet"
href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.css">
</head>
<body>
<div id="payment"></div>
<script nonce="{{cspNonce}}"
src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.js"></script>
<script nonce="{{cspNonce}}">
async function startCheckout() {
const sessionResponse = await fetch('/api/adyen/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin'
});
const session = await sessionResponse.json();
const checkout = await AdyenCheckout({
environment: 'test',
clientKey: '{{ADYEN_CLIENT_KEY}}',
session,
onPaymentCompleted: (result) => {
console.log('Payment completed', result);
window.location.href = '/checkout/success';
},
onError: (error) => {
console.error(error);
}
});
checkout.create('dropin').mount('#payment');
}
startCheckout();
</script>
</body>
</html>
If you don’t use nonces and rely on 'unsafe-inline' for scripts, you’re weakening the page that handles payment setup. That’s exactly the page I’d keep the tightest.
Express example: generate and send the CSP header
Here’s a simple Node/Express setup that generates a nonce per request.
import express from 'express';
import crypto from 'crypto';
const app = express();
function buildCsp(nonce) {
return [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://checkoutshopper-test.adyen.com`,
"style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com",
"img-src 'self' data: https:",
"font-src 'self' https://checkoutshopper-test.adyen.com",
"connect-src 'self' https://checkoutshopper-test.adyen.com",
"frame-src 'self' https://checkoutshopper-test.adyen.com",
"report-uri /csp-report"
].join('; ');
}
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', buildCsp(nonce));
next();
});
app.get('/checkout', (req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.css">
</head>
<body>
<div id="payment"></div>
<script nonce="${res.locals.cspNonce}" src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.js"></script>
<script nonce="${res.locals.cspNonce}">
console.log('Boot Adyen here');
</script>
</body>
</html>
`);
});
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/reports+json'] }), (req, res) => {
console.log('CSP violation:', JSON.stringify(req.body, null, 2));
res.sendStatus(204);
});
app.listen(3000);
Extending your existing site policy
If your current site policy looks like the headertest.com example, don’t dump Adyen domains into the global policy unless checkout runs everywhere.
Make a route-specific CSP for /checkout and related payment pages.
For example, your existing policy has:
script-src 'self' 'nonce-YmZkODljYWItY2ZiNC00Y2U2LTg4MjEtY2VhOGY1NDA2N2Y1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
For checkout, evolve it like this:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://checkoutshopper-test.adyen.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://checkoutshopper-test.adyen.com;
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://checkoutshopper-test.adyen.com https://your-backend.example;
frame-src 'self' https://consentcdn.cookiebot.com https://checkoutshopper-test.adyen.com;
font-src 'self' https://checkoutshopper-test.adyen.com;
That’s much better than widening default-src and hoping everything works.
Common breakages
1. Card fields or 3DS challenge never render
Usually frame-src is missing the Adyen origin.
Check browser console errors like:
Refused to frame 'https://checkoutshopper-test.adyen.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".
Fix:
frame-src 'self' https://checkoutshopper-test.adyen.com;
2. Payment methods fail to load
Usually connect-src is too narrow.
Adyen’s frontend makes network requests. If those are blocked, the UI may show a spinner forever.
Fix:
connect-src 'self' https://checkoutshopper-test.adyen.com https://your-api.example;
3. Adyen JS loads, but CSS or icons look broken
You probably forgot style-src, font-src, or a broad enough img-src.
Fix:
style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
font-src 'self' https://checkoutshopper-test.adyen.com;
img-src 'self' data: https:;
4. Your own inline setup script is blocked
Good. That means CSP is doing its job.
Use a nonce:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;
Then add the same nonce to the script tag.
Report-only first, then enforce
For payment flows, I like to deploy in two steps:
Content-Security-Policy-Report-Only- real
Content-Security-Policy
That gives you time to catch weird edge cases like wallet flows, challenge windows, or region-specific endpoints before customers hit them.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;
style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
connect-src 'self' https://checkoutshopper-test.adyen.com https://your-api-origin;
frame-src 'self' https://checkoutshopper-test.adyen.com;
img-src 'self' data: https:;
font-src 'self' https://checkoutshopper-test.adyen.com;
report-uri /csp-report;
Keep checkout isolated
If there’s one thing I’d push hard, it’s this: isolate your payment CSP from the rest of your app.
Checkout pages should have:
- fewer analytics tags
- fewer third-party scripts
- fewer experiments
- fewer widgets
- a tighter CSP than the rest of the site
Every extra origin in script-src, connect-src, or frame-src is another thing that can break or be abused.
For official implementation details, check Adyen’s own documentation for the specific Web version and payment methods you use, because the exact endpoints and asset hosts can differ by setup. That part matters.
A practical rollout looks like this:
- build a dedicated checkout CSP
- add only the Adyen origins you actually need
- use nonces for inline bootstrapping
- test 3DS and alternative payment methods
- run report-only first
- enforce once violations are clean
That approach is boring, strict, and reliable. For payments, boring and reliable wins every time.