CSP for Klarna Payments: Policies That Actually Work
Table of Contents
If you add Klarna to checkout and keep a strict Content Security Policy, you’ll hit the usual wall: payment providers love scripts, frames, API calls, and redirects. CSP hates all of that by default.
The trick is not to weaken your whole site just to make one payment method work.
I’d treat Klarna like any other high-risk third-party integration: isolate what it needs, keep the rest of the app locked down, and verify the final policy in a real browser with CSP violation reporting turned on.
What Klarna usually needs from CSP
A Klarna integration commonly needs permission for:
- JavaScript loaded from Klarna domains
- embedded frames for payment UI
- XHR/fetch calls to Klarna APIs
- images and styles used by the widget
- redirects or popup flows depending on the checkout model
The exact domains vary by region, environment, and product:
- Payments
- On-site messaging
- Hosted payment page
- identity or auth-related flows
- sandbox vs production
That means you should not cargo-cult a random CSP snippet and hope for the best.
Start from a strict baseline, then add only what Klarna actually uses in your integration.
Start with a strict baseline
Here’s a solid baseline policy for a modern app before Klarna is added:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
I like this pattern because it keeps the surface area small:
default-src 'self'blocks everything else by defaultscript-srcuses a nonce instead of host allowlists where possibleobject-src 'none'kills old plugin garbageframe-ancestors 'none'prevents clickjacking unless you intentionally embed your app elsewhere
For reference, a real-world CSP often grows around analytics and consent tooling. Here’s the header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZWQzYzE3YzYtYzQzMS00Njg0LThmMzQtNTRiNDc2ZDFhNGE5' '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 good reminder that payment CSP does not live in a vacuum. Your final policy has to coexist with analytics, consent banners, fraud tooling, and your own frontend stack.
Add Klarna incrementally
A typical Klarna setup needs additions like these:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://*.klarna.com;
style-src 'self' 'unsafe-inline' https://*.klarna.com;
img-src 'self' data: https://*.klarna.com;
connect-src 'self' https://*.klarna.com;
frame-src 'self' https://*.klarna.com;
object-src 'none';
base-uri 'self';
form-action 'self' https://*.klarna.com;
frame-ancestors 'none';
This is a starting point, not a final answer.
Why these directives matter:
script-src: Klarna widgets or SDK bootstrapsstyle-src: some hosted widgets inject or require inline stylesimg-src: logos, payment badges, tracking pixelsconnect-src: API calls, session setup, token exchangeframe-src: embedded payment UIform-action: hosted or redirected flows can submit to Klarna endpoints
If your implementation uses a popup or redirect, navigate-to can also matter in newer CSP strategies, though browser support and operational value are mixed enough that I don’t usually lead with it.
Sandbox and production often differ
This is where teams break checkout in staging and then “fix” it by adding https: everywhere. Don’t do that.
Use environment-specific policies.
Example: production
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$csp_nonce' 'strict-dynamic' https://js.klarna.com;
style-src 'self' 'unsafe-inline' https://js.klarna.com;
img-src 'self' data: https://js.klarna.com https://*.klarna.com;
connect-src 'self' https://api.klarna.com https://*.klarna.com;
frame-src 'self' https://js.klarna.com https://*.klarna.com;
form-action 'self' https://*.klarna.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
" always;
Example: sandbox
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$csp_nonce' 'strict-dynamic' https://js.playground.klarna.com;
style-src 'self' 'unsafe-inline' https://js.playground.klarna.com;
img-src 'self' data: https://js.playground.klarna.com https://*.playground.klarna.com;
connect-src 'self' https://api.playground.klarna.com https://*.playground.klarna.com;
frame-src 'self' https://js.playground.klarna.com https://*.playground.klarna.com;
form-action 'self' https://*.playground.klarna.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
" always;
Check Klarna’s official docs for the exact domains your product uses. Their integration docs are the only source I’d trust over browser telemetry. Start with Klarna Developers documentation.
Express example with nonces
If your checkout page renders server-side, generate a nonce per request and use it for your own inline bootstrap code.
import express from "express";
import crypto from "crypto";
const app = express();
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
next();
});
app.get("/checkout", (req, res) => {
const nonce = res.locals.cspNonce;
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://js.klarna.com`,
"style-src 'self' 'unsafe-inline' https://js.klarna.com",
"img-src 'self' data: https://js.klarna.com https://*.klarna.com",
"connect-src 'self' https://api.klarna.com https://*.klarna.com",
"frame-src 'self' https://js.klarna.com https://*.klarna.com",
"form-action 'self' https://*.klarna.com",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join("; ")
);
res.send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Checkout</title>
</head>
<body>
<div id="klarna-payments-container"></div>
<script nonce="${nonce}">
window.checkoutConfig = {
clientToken: "REPLACE_ME"
};
</script>
<script
nonce="${nonce}"
src="https://js.klarna.com/web-sdk/v1/klarna.js">
</script>
</body>
</html>
`);
});
app.listen(3000);
A couple of opinions here:
- I’d rather use nonces than rely on broad script host allowlists.
- I only add
'unsafe-inline'tostyle-srcwhen the vendor forces my hand. - I would never add
'unsafe-inline'toscript-srcfor a payment flow.
If Klarna is embedded only on checkout, scope the CSP there
Don’t give Klarna permissions on your marketing pages, account settings, docs pages, and admin dashboard.
Use a route-specific CSP.
Example with Helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.use("/checkout", helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://js.klarna.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://js.klarna.com"],
imgSrc: ["'self'", "data:", "https://js.klarna.com", "https://*.klarna.com"],
connectSrc: ["'self'", "https://api.klarna.com", "https://*.klarna.com"],
frameSrc: ["'self'", "https://js.klarna.com", "https://*.klarna.com"],
formAction: ["'self'", "https://*.klarna.com"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"]
}
}
}));
For the rest of the site, keep a tighter global policy.
If you want prebuilt policy patterns, csp-examples.com is handy for quick comparisons.
Common Klarna CSP breakages
1. connect-src is too tight
The widget loads, then payment session creation fails in the background.
Browser console usually shows something like:
Refused to connect to 'https://api.klarna.com/...' because it violates the following Content Security Policy directive: "connect-src 'self'".
Fix: add the exact API hosts used by your flow.
2. Frames are blocked
Klarna often renders inside an iframe or launches a framed step during authorization.
Refused to frame 'https://...' because it violates the following Content Security Policy directive: "frame-src 'self'".
Fix: expand frame-src for Klarna domains.
3. Form submission is blocked
Hosted payment pages and redirect-based flows can submit to external origins.
Refused to send form data to 'https://...' because it violates the following Content Security Policy directive: "form-action 'self'".
Fix: allow the specific Klarna form targets.
4. You forgot your other vendors
A payment page often contains:
- tag manager
- analytics
- consent platform
- fraud script
- A/B testing
That’s how you end up with a policy like the headertest.com example. The real work is making Klarna fit without turning the page into script-src https: nonsense.
Roll out with Report-Only first
I nearly always ship payment CSP changes in report-only mode before enforcing them.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://js.klarna.com;
style-src 'self' 'unsafe-inline' https://js.klarna.com;
img-src 'self' data: https://js.klarna.com https://*.klarna.com;
connect-src 'self' https://api.klarna.com https://*.klarna.com;
frame-src 'self' https://js.klarna.com https://*.klarna.com;
form-action 'self' https://*.klarna.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-to csp-endpoint;
Then exercise the full flow:
- load checkout
- choose Klarna
- open any modal or embedded UI
- complete auth
- submit payment
- test failure paths too
The happy path isn’t enough. Payment integrations often hit different endpoints on retries, cancellations, and soft declines.
A practical final policy
Here’s a realistic checkout CSP that includes your own app, Klarna, and leaves room for a consent or analytics layer similar to the headertest.com example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic'
https://js.klarna.com
https://www.googletagmanager.com
https://*.cookiebot.com
https://*.google-analytics.com;
style-src 'self' 'unsafe-inline'
https://js.klarna.com
https://www.googletagmanager.com
https://*.cookiebot.com
https://consent.cookiebot.com;
img-src 'self' data: https:
https://*.klarna.com;
font-src 'self';
connect-src 'self'
https://api.klarna.com
https://*.klarna.com
https://*.google-analytics.com
https://*.googletagmanager.com
https://*.cookiebot.com;
frame-src 'self'
https://*.klarna.com
https://consentcdn.cookiebot.com;
form-action 'self' https://*.klarna.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';
I’d still tighten this further once I know the exact Klarna endpoints from production traffic and official docs.
That’s the real job with CSP for payments: start strict, test the actual flow, and resist the temptation to solve breakage with giant wildcards. Payment pages deserve better than that.