PSPDFKit viewer is one of those libraries that can blow up a strict CSP if you treat it like a plain old script include. PDF rendering, workers, fonts, blobs, WebAssembly, and sometimes remote assets all show up fast.
I’ve had the best results by starting with a very tight policy, then opening only what the viewer actually needs in your setup. That matters because PSPDFKit deployments vary a lot:
- self-hosted assets
- CDN-hosted assets
- standalone browser viewer
- server-backed mode
- WebSocket features
- blob/object URLs for document rendering
- inline bootstrapping code from your app
So a working CSP for one app can still fail badly in another.
A practical baseline
If you self-host PSPDFKit assets on the same origin and initialize it from your own app code, this is a solid starting point:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
worker-src 'self' blob:;
child-src 'self' blob:;
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That policy assumes:
- PSPDFKit JavaScript is served from your own origin
- viewer workers are loaded from your own origin or blob URLs
- images/fonts may be embedded as
data:or generated asblob: - no third-party analytics or tag managers are involved
If your app uses an inline script to bootstrap the viewer, you’ll need a nonce or hash.
Copy-paste example with a nonce
This is the version I’d use for most production apps:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
worker-src 'self' blob:;
child-src 'self' blob:;
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And the matching HTML:
<script nonce="rAnd0m123">
PSPDFKit.load({
container: "#pspdfkit",
document: "/documents/sample.pdf",
baseUrl: `${window.location.origin}/`
});
</script>
If you’re already using nonces elsewhere, keep it consistent. Don’t fall back to 'unsafe-inline' in script-src just because the viewer init snippet is inline. That’s the lazy fix and usually the wrong one.
When PSPDFKit needs blob, worker, or WASM support
This is where most CSP breakage happens.
Modern PDF viewers often use:
blob:URLs for generated resources- web workers for parsing/rendering
- WebAssembly binaries
- fetch/XHR calls to load documents, assets, or annotations
A more realistic policy for browser rendering looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: blob:;
connect-src 'self' https://api.example.com;
worker-src 'self' blob:;
child-src 'self' blob:;
media-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
If documents are fetched from another API origin, add that origin to connect-src.
Example:
connect-src 'self' https://api.example.com https://files.examplecdn.com;
If the PDF itself is loaded directly by URL and the browser treats it as a fetchable resource from another host, connect-src is usually the directive that matters.
If you host PSPDFKit assets on a CDN
A lot of teams do this at first, then forget CSP needs to reflect it.
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'nonce-rAnd0m123';
style-src 'self' 'unsafe-inline' https://cdn.example.com;
img-src 'self' data: blob: https://cdn.example.com;
font-src 'self' data: https://cdn.example.com;
connect-src 'self' https://api.example.com;
worker-src 'self' blob: https://cdn.example.com;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Replace https://cdn.example.com with the actual PSPDFKit asset origin you use.
If you load scripts with a nonce and also dynamically load more scripts, you may want strict-dynamic. That can work well, but only if you understand the trust model. If you need a refresher on directive behavior, the reference at https://csp-guide.com is useful.
A stricter policy with strict-dynamic
This pattern is common in modern apps that bootstrap trusted scripts with a nonce:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' https://api.example.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That said, I wouldn’t add strict-dynamic unless you actually need it. It makes sense for script-loader heavy apps. It’s not automatically better just because it looks more advanced.
Real-world policy pattern with third-party noise
Most production sites don’t run a viewer in isolation. They have analytics, consent banners, A/B test junk, and five scripts nobody wants to remove.
Here’s the real CSP header you provided from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YmQ5NmVhZWUtNDdkNC00NTY4LTgzNDQtMGNjZGFmNGFmZjU5' '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'
If you adapt that kind of policy for PSPDFKit, you’d typically add the viewer-specific pieces rather than rewriting everything:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YmQ5NmVhZWUtNDdkNC00NTY4LTgzNDQtMGNjZGFmNGFmZjU5' '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: blob: https:;
font-src 'self' data: blob:;
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://api.example.com;
worker-src 'self' blob:;
child-src 'self' blob:;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s usually the sane approach: keep your existing app policy, then add the PSPDFKit requirements:
blob:inimg-src- maybe
blob:infont-src worker-src 'self' blob:- document/API origins in
connect-src
Common directives that matter for PSPDFKit
script-src
Controls the viewer script and your initialization code.
Typical options:
script-src 'self' 'nonce-rAnd0m123';
Or with CDN:
script-src 'self' 'nonce-rAnd0m123' https://cdn.example.com;
worker-src
Very often required.
worker-src 'self' blob:;
If this is missing, workers may silently fail or throw CSP violations in the console.
connect-src
Used for API calls, document fetches, collaboration endpoints, and sometimes WebSockets.
connect-src 'self' https://api.example.com wss://realtime.example.com;
img-src
PDF rendering pipelines often produce blob/data-backed image resources.
img-src 'self' data: blob:;
style-src
A lot of UI libraries still need inline styles. I don’t love it, but 'unsafe-inline' in style-src is still common and much less dangerous than in script-src.
style-src 'self' 'unsafe-inline';
object-src
Set this to none unless you enjoy legacy plugin risk for no benefit.
object-src 'none';
Nginx example
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-rAnd0m123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data: blob:; connect-src 'self' https://api.example.com; worker-src 'self' blob:; child-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';" always;
Express example
app.use((req, res, next) => {
const nonce = crypto.randomUUID();
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data: blob:",
"connect-src 'self' https://api.example.com",
"worker-src 'self' blob:",
"child-src 'self' blob:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join("; ")
);
res.locals.cspNonce = nonce;
next();
});
Template:
<script nonce="{{cspNonce}}">
PSPDFKit.load({
container: "#pspdfkit",
document: "/documents/sample.pdf"
});
</script>
Debugging checklist
When PSPDFKit breaks under CSP, I check these first:
-
Worker blocked
- Add
worker-src 'self' blob:
- Add
-
Document/API fetch blocked
- Fix
connect-src
- Fix
-
Rendered assets blocked
- Add
blob:anddata:toimg-src - Sometimes
font-srctoo
- Add
-
Inline bootstrap script blocked
- Use a nonce in
script-src
- Use a nonce in
-
CDN asset blocked
- Add the exact asset origin to
script-src,style-src,font-src, orworker-srcas needed
- Add the exact asset origin to
-
WASM-related issues
- Check the browser console carefully; depending on packaging and browser behavior, the underlying issue may still present as script, worker, or fetch restrictions
My recommended default
If you want one policy to start with for most PSPDFKit viewer integrations, use this and adjust from there:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: blob:;
connect-src 'self' https://api.example.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That’s not magically universal, but it covers the failures I see most often without turning CSP into decorative theater.