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 default
  • script-src uses a nonce instead of host allowlists where possible
  • object-src 'none' kills old plugin garbage
  • frame-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 bootstraps
  • style-src: some hosted widgets inject or require inline styles
  • img-src: logos, payment badges, tracking pixels
  • connect-src: API calls, session setup, token exchange
  • frame-src: embedded payment UI
  • form-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' to style-src when the vendor forces my hand.
  • I would never add 'unsafe-inline' to script-src for 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.