DartPad-style apps are a weird CSP target.
You’re not just serving a normal frontend. You’ve got inline bootstrapping code, dynamic script loading, iframes, workers, API calls, maybe WebSockets, and often some analytics or consent tooling bolted on top. That combination is exactly where people give up and slap unsafe-inline and unsafe-eval into production.
I’ve done that under deadline pressure. I’ve regretted it every time.
If you’re building something like DartPad, the goal is to allow the platform features you actually need without turning CSP into decorative security theater.
What makes DartPad tricky
A DartPad-like app usually needs some or all of this:
- App shell from your own origin
- Embedded editor UI
- Sandboxed preview iframe
- Compiler or execution service over
fetch()or WebSocket - Web workers
- Dynamically injected scripts during bootstrap
- Analytics or consent scripts
- Inline config passed from server to client
That means a serious CSP has to cover at least:
script-srcstyle-srcconnect-srcframe-srcworker-srcimg-srcbase-uriform-actionobject-srcframe-ancestors
If you skip those, you’ll either break the app or leave obvious holes.
Start from a real-world header
Here’s a real CSP header seen on headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NzE5Yjc5ZWQtMjI1MC00OGQxLWE5YWQtMDE0N2Q5ODM0NTM3' '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 decent example of a modern policy shape:
strict-dynamicplus a nonce for scripts- locked-down framing with
frame-ancestors 'none' - no plugins via
object-src 'none' - explicit network destinations with
connect-src
For a DartPad deployment, I’d use that style as a starting point, then adapt it for workers, preview frames, and backend execution endpoints.
A practical CSP for DartPad
Here’s a good first-pass policy for a self-hosted DartPad-like app:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' https://api.example.com wss://runner.example.com;
worker-src 'self' blob:;
frame-src 'self' blob:;
child-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
A few opinions here:
- I prefer nonce-based script loading over host allowlists when possible.
- I only keep
style-src 'unsafe-inline'if the app or framework really needs it. A lot of UI systems still do. worker-srcmatters for browser-based compilation or sandbox helpers.blob:often ends up necessary for workers or generated preview content.frame-ancestors 'none'is the right default unless embedding is a product feature.
If your app is embedded somewhere else on purpose, replace:
frame-ancestors 'none';
with something explicit:
frame-ancestors 'self' https://docs.example.com;
Nonces for your bootstrap script
A DartPad frontend often needs one tiny inline script to pass config into the app. That’s exactly what nonces are for.
Server-side example in Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
app.get("/", (req, res) => {
const nonce = crypto.randomBytes(16).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self'",
"connect-src 'self' https://api.example.com wss://runner.example.com",
"worker-src 'self' blob:",
"frame-src 'self' blob:",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"object-src 'none'"
].join("; ");
res.setHeader("Content-Security-Policy", csp);
res.send(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>DartPad Clone</title>
</head>
<body>
<div id="app"></div>
<script nonce="${nonce}">
window.DARTPAD_CONFIG = {
apiBase: "https://api.example.com",
runnerSocket: "wss://runner.example.com"
};
</script>
<script nonce="${nonce}" src="/assets/main.js"></script>
</body>
</html>
`);
});
app.listen(3000);
That gives you two nice properties:
- The inline config script is allowed.
- Your trusted bootstrap script can load other scripts because of
strict-dynamic.
If you’re fuzzy on strict-dynamic, the directive is worth reading up on at https://csp-guide.com/strict-dynamic/.
Don’t use unsafe-inline for scripts
For DartPad specifically, this is the trap.
Bad:
script-src 'self' 'unsafe-inline' 'unsafe-eval';
That’s basically saying, “Please ignore the security boundary.”
If your app breaks without unsafe-eval, figure out why. Sometimes it’s a dev build, a source-map helper, or an outdated dependency. Fix the root cause instead of normalizing it in production.
Workers and blob URLs
Browser-based code runners and preview pipelines often create workers from generated code. That usually means blob:.
Example:
const workerCode = `
self.onmessage = (event) => {
const result = event.data.toUpperCase();
self.postMessage(result);
};
`;
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (event) => {
console.log(event.data);
};
worker.postMessage("hello");
Without this CSP:
worker-src 'self' blob:;
that worker will fail.
Some apps also need:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' blob:;
I’d avoid adding blob: to script-src unless you’ve confirmed it’s required. Keep it scoped to worker-src first.
Preview iframes
DartPad-style tools often render output in an iframe. If the preview is served from your own origin, frame-src 'self' is enough.
If you generate preview documents using blob: URLs, allow that too:
frame-src 'self' blob:;
Example:
const html = `
<!doctype html>
<html>
<body>
<h1>Preview</h1>
<script>console.log("running preview")</script>
</body>
</html>
`;
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
document.querySelector("#preview").src = url;
For untrusted user code, I’d also sandbox the iframe in markup:
<iframe
id="preview"
sandbox="allow-scripts"
referrerpolicy="no-referrer">
</iframe>
CSP helps, but iframe sandboxing is a separate control. Use both.
Network access: connect-src
This directive gets messy fast in editor apps.
A DartPad deployment may need:
- API requests for compile/run
- WebSocket connection for live execution logs
- telemetry endpoints
- maybe package metadata or snippet storage
Example:
connect-src 'self' https://api.example.com wss://runner.example.com;
If your app suddenly can’t compile code, this is one of the first directives to check.
The headertest.com policy is a good illustration of how real apps expand here:
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;
That’s normal. connect-src tends to accumulate reality.
Analytics and consent scripts
If you add Google Tag Manager, analytics, or Cookiebot, your CSP stops being “clean.” That’s just how it goes.
A policy in the style of the real header might look like this:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-{RANDOM_NONCE}' '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.example.com wss://runner.example.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
I don’t love broad third-party allowances, but if product requires them, at least make them explicit.
Debug with Report-Only first
When tightening CSP on an existing app, I always start with Content-Security-Policy-Report-Only.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.example.com wss://runner.example.com;
worker-src 'self' blob:;
frame-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That lets you see what would break before you enforce it.
If you want deeper directive references, the official docs at https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy are still the first place I check.
A sane baseline
If I were shipping CSP for DartPad today, I’d start here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' https://api.example.com wss://runner.example.com;
worker-src 'self' blob:;
frame-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
Then I’d add only what the app proves it needs.
That’s the real trick with CSP on complex apps like DartPad: don’t start permissive and promise to clean it up later. You probably won’t. Start tight, watch violations, and expand deliberately.