Skip to content

Security & Compliance

CSP for a blob: iframe that prints a PDF

Why printing a PDF from a hidden iframe needs blob: in frame-src, not a data: URL, what the frame inherits from your policy, and what Chrome's PDF viewer does.

Ridam Agrawal

Founder, Easinvy

Published
Read
7 min
On this page

In short

How do you allow a blob: URL in an iframe with CSP?

Add blob: to frame-src, because 'self' does not match it. Easinvy Retail also needs the frame same-origin, since print() cannot be called on a cross-origin window, which rules out data: URLs. The blob document then inherits the page's whole policy, so the browser's own PDF viewer runs under your style-src.

Everything below is the print path of a live retail billing system and the policy it runs under, read on 26 September 2026. The print code dates from 11 September 2026 and the policy from 14 September. The rest of that policy, and why Razorpay is in it as a wildcard, is in Content Security Policy for Razorpay Checkout.

Printing a bill from a hidden frame

Easinvy Retail prints a bill by loading its PDF into an iframe the shop never sees and calling that frame's own print dialog. The code, trimmed to the lines that matter, from lib/bill-print.ts:

const response = await fetch(buildBillPdfUrl(billId, "inline"), { method: "GET" })
const blobUrl = window.URL.createObjectURL(await response.blob())
const frame = document.createElement("iframe")
frame.style.width = "1px"
frame.style.height = "1px"
frame.style.opacity = "0"

frame.onload = () => {
  window.setTimeout(() => {
    frame.contentWindow?.focus()
    frame.contentWindow?.print()
  }, PDF_RENDER_DELAY_MS)
}

document.body.appendChild(frame)
frame.src = blobUrl

A frame rather than a new tab, for a reason the file records: "A frame's own print() is not subject to the popup blocker, which matters here: Save & Print reaches this after an await, by which point a window.open would be refused as unrequested."

The 700 ms wait is there because Chrome fires load too early. The comment: "Chrome fires load when the PDF viewer attaches, not when it has painted the bill, and printing in that gap puts a blank sheet in the dialog before the bill replaces it."

Two lines in the policy make this work: frame-src 'self' blob: and, as the rest of this note explains, a style-src that the policy's own comment gives a different reason for.

Why a blob and not a data: URL

The parent page calls print() on the frame's window. Whether it is allowed to depends on whether the two share an origin.

The HTML Standard lists exactly which properties of a Window another origin may touch. There are thirteen: window, self, location, close, closed, focus, blur, frames, length, top, opener, parent and postMessage. For anything else, "If IsPlatformObjectSameOrigin(platformObject) is false, then throw a "SecurityError" DOMException." print is not on the list.

A blob: URL created by the page carries the page's origin, so the frame is same-origin and print() runs. A data: URL has an opaque origin. In that frame the focus() call would succeed, because focus is one of the thirteen, and the print() on the very next line would throw.

That is the reason to use blob: here, and it is the case the Stack Exchange question Google ranks first for this search does not cover. Its one answer says: "A more secure alternative are data: URLs which have an opaque origin. Additionally, you should sandbox the iframe." For a viewer of files somebody uploaded, that is good advice. For a frame that has to print, it is the one arrangement that cannot.

Why 'self' does not cover blob:

frame-src 'self' alone refuses the frame. Content Security Policy Level 3 changed how sources match: it now requires "explicit presence of any non-HTTP(S) scheme, rather than local scheme, unless that non-HTTP(S) scheme is the same as the scheme of protected resource". The page is https:, so blob: has to be named.

The deployed line is frame-src 'self' blob: https://*.razorpay.com. MDN's page for frame-src gives the directive and its fallback to child-src and default-src, and does not say that 'self' excludes blob:. The spec does.

Browsers also disagree about when frame-src applies to a blob at all. An Office add-in issue from 2020 describes a blob download refused by frame-src in Firefox, while it "worked and still works in Chrome".

What the frame inherits

A blob document is not a fresh page with no policy. Content Security Policy Level 3: "Documents loaded from local schemes will inherit a copy of the policies in the source document." The stated goal is that a page cannot slip out of its own policy by framing content it controls, and blob URLs are named among the examples. The print frame runs under the billing page's policy.

Which document counts as the source is where Chrome and the spec part. A Chromium engineer filed it in November 2020 as issue 40053796: "At the moment, blob: urls inherit their policy container from the navigation initiator. This is not correct: they should get the same policies of the document which created the blob." It was still open on 26 September 2026.

For this product the two answers are the same. The billing page creates the blob with createObjectURL and the same page navigates the frame to it with frame.src = blobUrl, so creator and initiator are one document. The difference would show where one document makes a blob and another opens it.

Issue 488072238 covers the navigation side. For the method used here it says "For src assignment, the target <iframe> should inherit from the parent." It was closed on 2 March 2026 as Won't Fix (Intended Behavior), with the note "As for blob: URLs, inheritance in chrome is currently not correct, and this is tracked as https://crbug.com/40053796. We don't consider this a security vulnerability though."

Chrome's PDF viewer runs under your style-src

Because the frame inherits the page's policy, the browser's own PDF viewer runs under it too, and Chrome's viewer uses inline styles. Issue 343754409, filed by a Chromium engineer in May 2024, describes exactly this: a strict policy, a PDF in an iframe as a blob URL, and CSP violations from the viewer's own inline styles. Its workaround, verbatim: "A not great workaround is to add style-src unsafe-inline to the embedding page's CSP."

A fix was reported in August 2025 as having "landed in version 141.0.7342.0", and a comment in October 2025 says "Not fixed in OOPIF PDF, issue still there." The issue was still Assigned on 26 September 2026, and a later comment calls the violations "non-critical".

Easinvy Retail's policy has style-src 'self' 'unsafe-inline'. Its comment gives Next.js's inline styles as the reason. The same line keeps Chrome's PDF viewer inside the policy, which the comment does not mention. What the viewer's violations would do to a printed bill without it was not tested.

Keep the frame until the dialog is gone

A print frame cannot be cleaned up the moment print() returns. The comment in bill-print.ts: "A print frame cannot be torn down while its dialog is open — removing it, or revoking its blob URL, closes the dialog and the bill goes unprinted."

Nothing reliably says when the dialog has closed. "A framed PDF is a plugin document, so it fires no afterprint", and window focus fires for too many other reasons to use. The bug that taught this was Save & Print going back to the bills list a moment after printing, which on its own closed the dialog. It was fixed on 11 September 2026 in a commit titled "Stop the print dialog closing itself a second after it opens".

So the frame is retired on one of two certainties: the next print has started, or two minutes have passed. Until then it sits in the page at one pixel. Only then is revokeObjectURL called.

Is it safe to allow blob: here

The same Stack Exchange answer's warning is accurate: "Allowing URLs with the blob: scheme is risky, because they inherit the origin from the environment that created them." A blob URL has your page's origin, so whatever is inside it runs as your page.

That matters when the blob holds something a user supplied. Here it holds a PDF the product's own server generated for the signed-in shop, and the shared origin is what makes printing possible. A sandboxed frame is given an opaque origin unless allow-same-origin is set, which puts print() back out of reach for the same reason a data: URL does.

What this does not check

Print is confirmed in Chrome only. The owner confirmed on 26 September 2026 that a bill printed from Chrome under this policy appeared; no date for that print was recorded. The product's own check when the policy shipped recorded that "a blob: iframe attaches", not that a PDF painted. Firefox and Safari have not been tested.

Two of the policy's blob: entries are not accounted for by this path. The comment says img-src blob: is needed for printing, but nothing tested it and nothing in the product displays a blob as an image. No reason was ever written for worker-src 'self' blob:. Neither is changed here.

This is one product's print path and the policy it needs, not a security review. How Easinvy approaches this work is under security and compliance, and the product this prints bills for is Easinvy Retail.

Questions people ask

Is it safe to allow blob: in frame-src?
A blob URL carries the origin of the page that made it, so it is as safe as what goes into it. For uploaded files, prefer a sandboxed frame. Easinvy Retail frames only PDFs its own server generated, and needs the same origin to print them.
Does 'self' include blob: in CSP?
No. CSP Level 3 requires any scheme other than http and https to be listed explicitly, so a blob URL is refused by frame-src 'self' alone. Easinvy Retail lists blob: in frame-src, img-src and worker-src by name.

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