Skip to content

Security & Compliance

Content Security Policy for Razorpay Checkout

The Content Security Policy a live billing app ships with Razorpay Checkout, line by line: why a wildcard, why unsafe-inline, and how to prove it is on.

Ridam Agrawal

Founder, Easinvy

Published
Read
6 min
On this page

In short

What CSP does Razorpay Checkout need?

Razorpay Checkout needs its hosts in script-src, frame-src, connect-src and img-src. Easinvy Retail allows https://*.razorpay.com in all four rather than naming subdomains, because the checkout.js it loads referenced eight Razorpay subdomains on 25 September 2026, was modified the day before, and builds another API host from a region setting.

Everything below is the Content Security Policy that Easinvy Retail, a billing app for shops, sends with every page. The policy went live on 14 September 2026; the header was read back from production, and the Razorpay Checkout script it allows was fetched and counted, on 25 September 2026. Razorpay does not appear to publish a CSP host list for Checkout, and the top results for this search are three other payment companies' pages. What follows is one real policy, the reason written down for each line, and the check that proves it is switched on.

The whole policy, line by line

Set beside the policy Next.js documents for apps that do not use nonces, this one differs in six places. As deployed:

default-src 'self'
script-src 'self' 'unsafe-inline' https://*.razorpay.com https://www.googletagmanager.com
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob: https://*.razorpay.com https://*.google-analytics.com https://www.googletagmanager.com
font-src 'self' data:
connect-src 'self' https://*.razorpay.com https://*.google-analytics.com https://*.analytics.google.com https://www.googletagmanager.com
frame-src 'self' blob: https://*.razorpay.com
worker-src 'self' blob:
object-src 'none'
base-uri 'self'
form-action 'self'
frame-ancestors 'none'
Change from the Next.js exampleWhy
Razorpay in script-src, img-src, connect-src, frame-srcCheckout is a script that opens a frame, sends telemetry and loads images
Google hosts in script-src, img-src, connect-srcGoogle Analytics 4
frame-src 'self' blob:Printing a bill frames a blob: URL of its PDF
data: in font-srcThe build self-hosts fonts and inlines the smallest subsets as data:
worker-src 'self' blob:No reason recorded — see the last section
upgrade-insecure-requests removed"it silently rewrites a mistake rather than surfacing it"

style-src 'unsafe-inline' is in the Next.js example too. The product's comment gives Next's inline styles as the reason. The same line does a second job the comment never names: the print frame inherits this policy, and Chrome's PDF viewer injects inline styles into it, which Chromium issue 343754409 has tracked since May 2024.

Why script-src keeps 'unsafe-inline'

A Next.js App Router page ships its hydration data as inline scripts. A strict policy allows those with a nonce, and the Next.js CSP guide is plain about the cost: "you must use dynamic rendering to add nonces", because "Static pages are generated at build time, when no request or response headers exist". For Easinvy's marketing pages, that would mean no CDN caching. The guide also describes a hash-based alternative using Subresource Integrity, marked experimental; it is not used here.

So the policy keeps 'unsafe-inline', and the comment above it says what that concedes:

The honest reading is that this CSP is not an XSS defence for scripts; it is a defence against exfiltration — connect-src, form-action and base-uri below are the ones doing work, and they are the ones an injected script would need.

The admin panel of the same product, a Vite single-page app on its own subdomain, got its policy the same day with script-src 'self' and no 'unsafe-inline'. The commit gives the reason: "Vite emits no inline script". Same team, same day, and the directive that matters most for cross-site scripting was decided by the framework, not by the team.

A wildcard, not four subdomains

Razorpay Checkout is allowed as https://*.razorpay.com, not as a list of named hosts. The reason is in the config:

Checkout loads from checkout., talks to api. and lumberjack., and images come from cdn.; a fifth appearing in their next release would be a payment page that silently stops working for every shop at once. The wildcard is the deliberate trade.

On 25 September 2026 the script at checkout.razorpay.com/v1/checkout.js was 190,150 bytes, last modified the day before, and cached for two minutes. Counted in its source, eight Razorpay subdomains:

HostWhere it appears
cdn.razorpay.comassets
api.razorpay.comthe API
lumberjack.razorpay.comtelemetry
lumberjack-cx.razorpay.comtelemetry
lumberjack-metrics.razorpay.commetrics, sent with sendBeacon
api-dark.razorpay.coman alternate API
express.razorpay.comexpress checkout
checkout-static-next.razorpay.combuild assets

The script also builds a ninth host at runtime, api- plus a region code, which no fixed list can name in advance. A hostname in the source is not a request, and which of these a subscription checkout from India actually contacts was not measured. The fifth host the comment worried about already exists.

The payment companies that do publish host lists disagree about this. Square names exact hosts. Walley uses wildcards and says "The domain lists below are subject to change". Trust Payments shows what that costs a merchant who pinned hosts: its May 2026 notice added a new host with a deadline of 15 May 2026, and warned that "Failure to update your CSP will result in 3D Secure authentication failures".

How to know the policy is actually on

The Content Security Policy was checked before it shipped, against a production build in a browser, and the commit that added it on 14 September 2026 lists four checks:

the real checkout.js loads, window.Razorpay is a function
a blob: iframe attaches, which is how Print shows a bill
an api.razorpay.com frame is not refused
a fetch to example.com IS refused, naming connect-src

The commit message explains why the last one matters: "without it, a policy that allows everything passes the first three checks too."

Checking that Checkout still loads cannot tell an enforced policy from a missing one. A header misspelt, set on the wrong route, or left in report-only mode passes every check that the payment still works. The only proof that a policy is enforced is a request it refuses: pick a host that is on no allowlist, fetch it from the page, and read the console. The browser names the directive that did the refusing, and that is the same message to read when something legitimate gets blocked.

What this policy does not stop

The comment says this policy defends against exfiltration: a script that got into the page could only send data to hosts the policy allows. The CSP specification agrees that it can, with a condition: "A policy's exfiltration mitigation ability depends upon the least-restrictive directive allowlist."

The least restrictive entry in this connect-src is https://*.google-analytics.com. Google Analytics accepts events for any property, including one an attacker owns. In June 2020 two security firms reported card skimmers using exactly that route against online stores. Kaspersky found that administrators "write *.google-analytics.com into the Content-Security-Policy header" and that the stolen data was "sent using the Google Analytics Measurement Protocol". Sansec put it more bluntly: "CSP is practically worthless when you already have Google Analytics on your site."

Both reports predate Google Analytics 4, but GA4 sends to the same domains, which is why they are on this allowlist. This is a limit, not a hole someone found: no injection point was looked for or found. The specification's own introduction calls CSP "defense-in-depth", and that is the most this policy claims to be.

Two comments that no longer match

The comments in this config were written with the policy on 14 September 2026, and two of them no longer match the code.

The first says Checkout "documents" four subdomains. On 25 September 2026 Razorpay's documentation had no page on Content Security Policy that could be found. Its security allowlist page lists IP addresses for firewalls and mentions neither CSP nor checkout.js. The four subdomains the comment names are the same four that Google's AI answer gives for this search. Where the comment got them is not recorded. The wildcard does not depend on it.

The second says the Google hosts are listed "even while GA4 has no Measurement ID yet". GA4 now has one and loads on every page, including billing. The reasoning was to add the hosts in the same change as the tag, before the ID, because a CSP block on analytics is silent. That worked exactly as intended. Only the tense is out of date.

What this does not check

No directive in the policy reports violations. It has no report-to or report-uri, so if a shop's browser ever refused something on the billing page, nothing would record it. The commit did not record which browser the checks ran in, and Firefox and Safari have not been tested.

Two directives have reasons that are unrecorded or unproven. A search of the app's code and of Razorpay's script found nothing that creates a worker, and no reason was written for worker-src 'self' blob:. The comment says img-src blob: is needed for printing, but the recorded check only tested the frame, and nothing in the product displays a blob: image.

None of this is a security review, and the policy is not a template for another site. It is one product's header with its reasons. More on how Easinvy approaches this work is under security and compliance.

Questions people ask

How to solve Content-Security-Policy error?
Read the console message: the browser names the directive that refused the resource and the URL it refused. Add that host to that directive only. Easinvy Retail tests the reverse too, confirming that a fetch to an unlisted host is refused.
How do I turn off the Content-Security-Policy?
Send Content-Security-Policy-Report-Only instead, which monitors violations without enforcing them. Easinvy Retail keeps its policy on in development and adds only unsafe-eval, which Next.js needs there, so a broken policy shows up before it is deployed.
How to update Content-Security-Policy?
Change the header where it is set, which in Next.js is the headers function in next.config, and redeploy. Easinvy Retail sets one policy for every route there, so a change reaches the billing page and the marketing pages together.

Sources

Ridam Agrawal

Founder and engineer at Easinvy. Writes up the parts of building and running a multi-tenant product that were expensive to get wrong. More about the studio →

Keep reading