Hint 1: A nonce switches 'unsafe-inline' off, not the other way round
Since CSP Level 2, a nonce or hash in script-src makes the browser ignore 'unsafe-inline', so it adds nothing in a current browser and only helps injected inline scripts in one too old for nonces. The starter's real hole is https:, which lets any HTTPS script load. Allow scripts by nonce and 'strict-dynamic' alone.
Hint 1 unlocks after your first failed run.
Hint 2: Name every directive that has a default of its own
default-src 'none' does not cover base-uri, frame-ancestors or form-action, so set each of them explicitly.
Hint 2 unlocks after 2 failed runs.
Reveal the reference patch and explanation
export function buildCsp(doc) {
const directives = [
"default-src 'none'",
"base-uri 'none'",
"object-src 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
"script-src 'nonce-" + doc.nonce + "' 'strict-dynamic'",
"style-src 'self'",
['img-src', "'self'", ...doc.img].join(' '),
['connect-src', "'self'", ...doc.connect].join(' '),
];
if (doc.reportTo) directives.push('report-to ' + doc.reportTo);
return directives.join('; ');
}The policy begins default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'. script-src is the nonce and 'strict-dynamic' only, so an injected inline or src script without the nonce does not run and a host allowlist cannot weaken it. style-src is 'self'. img-src and connect-src start with 'self' and then list exactly the declared origins, so a document that declares https://api.example.test for connect gets connect-src 'self' https://api.example.test and nothing wider. report-to is added only when a group is named.
Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.
The reference patch unlocks after you run code you have edited.
Inspect the actual test harness
The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.
Download cases.mjsHow the browser sandbox runs your code
Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.
The same as in current Node.js:
- ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag
Added to the sandbox so that it behaves like Node.js:
- structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
- URL and URLSearchParams, parsed by your browser’s own URL implementation
- TextEncoder and TextDecoder for UTF-8
- Intl.Collator and a locale-aware String.prototype.localeCompare
- console.log and related methods, whose output appears with your results
Not available in the browser sandbox:
- Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
- Node.js modules and globals: import, require, process and Buffer
- Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
- Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
- using declarations, Symbol.dispose and DisposableStack (explicit resource management)
- Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace
This builds the header value from declared needs; it does not generate the nonce (which must be unpredictable and fresh per response), send the Reporting-Endpoints header, or add the hashes an inline handler would need. CSP is defense in depth against injection, not a replacement for output encoding and a sanitizer. frame-ancestors and X-Frame-Options together stop framing; a page that must be embedded lists the embedding origin instead of 'none'. Roll a new policy out in report-only mode first.
Your next task: Does your application’s CSP still contain 'unsafe-inline' or a broad https: in script-src, and what would it take to move to nonces and 'strict-dynamic'?
Related guide: Configure HTTP security headers from a tested baseline