What does a Content Security Policy do?
A Content Security Policy, or CSP, is a response header that lists where a page may load things from: scripts, styles, images, fonts, frames. The browser checks every script before running it. If the script does not match the list, the browser refuses it and logs a message in the Console.
The main use is limiting the damage of cross-site scripting. If an attacker manages to get <img src=x onerror="steal()"> into your page, the onerror is an inline script. A policy that does not allow inline scripts stops it running, even though the markup reached the page. That is exactly what you saw in the Attacker view: blocked, with the reason saying the payload got in but CSP stopped it.
The policy is a second wall. The real fix for a script getting into the page is escaping the text that carried it, and the Attacker view says so even when CSP blocks the attack.
How do you read a policy?
A policy is a list of directives separated by semicolons. Each directive names a kind of content and lists allowed sources. 'self' means files from your own origin. A full address such as https://maps.example allows that site. 'none' allows nothing. The quotes are part of the keywords and are required.
default-src is the fallback for any kind you have not listed. script-src covers scripts, style-src covers styles, img-src images, connect-src the addresses fetch may call, and frame-ancestors which sites may frame your page. A short policy for the bird club could look like the one below.
Content-Security-Policy: default-src 'self'; img-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'Why does 'unsafe-inline' undo the policy?
Inline scripts are code written inside the page: a <script> block with no src, or an attribute such as onclick= or onerror=. An attacker who can add markup to a page nearly always uses inline script, because they usually cannot put a file on your server. Blocking inline script is therefore the main job of a policy.
'unsafe-inline' switches that protection off. The browser can no longer tell your inline script from one an attacker wrote, so it runs both. The name contains the word unsafe on purpose. A policy with script-src 'self' 'unsafe-inline' still blocks scripts from other sites, but those were never how this attack worked.


How do you keep your own scripts working?
The simplest route is the one you took in the task: move inline code into a .js file on your own site and load it with <script src>. 'self' allows it. Event handler attributes need the same treatment: replace onclick="showMap()" with an addEventListener('click', showMap) call in the file.
When you must keep a script inside the page, for example because a template prints data into it, use a nonce. The server makes a fresh random value for every response, puts it in the header as 'nonce-...', and adds the same value as a nonce attribute on its own <script> tags. The attacker cannot guess the value, so their script lacks it. A nonce never covers event handler attributes, only <script> elements. Once a directive contains a nonce, browsers ignore any 'unsafe-inline' in it, so the two do not mix.
The lab simplifies this: a nonce there is a fixed string in a file. On a real site a fixed nonce is worthless, since anyone can read it in the page source and copy it. Generate it per response, for example with crypto.randomBytes(16).toString('base64').
<!-- pages/index.html -->
<script src="/walks.js"></script>
// pages/walks.js
document.getElementById('next-walk').textContent = 'Next walk: Saturday, heron hide, 8am';What is Report-Only mode for?
A strict policy dropped onto an existing site often breaks something: an analytics snippet, a map widget, an old inline handler. Content-Security-Policy-Report-Only lets you test a policy without enforcing it. The browser reports each thing it would have blocked in the Console, and optionally to an address you choose, while letting everything run.
Run in Report-Only for a week, fix what it reports, then switch the header name to Content-Security-Policy. The trap is forgetting the second step. A Report-Only policy blocks nothing, so in the lab it leaves every attack open, and on a real site it does the same.
Where should the policy go?
Send it as a response header. A policy can also go in a <meta http-equiv="Content-Security-Policy"> tag inside the page, which helps on static hosts where you cannot set headers. The meta version has gaps: it cannot use frame-ancestors or reporting, and it only applies to content after the tag.
If several policies arrive, through two headers or a header and a meta tag, the browser enforces all of them. Content must pass every policy. Adding a second policy can only make things stricter, which surprises people who expect the newer one to replace the older.
How do you check a policy in the browser?
Open DevTools, go to the Console tab and reload. Every blocked script, style or image appears as a red message naming the directive that refused it, such as script-src. That message tells you which file or inline block to move, or which site to add to the list.
In the Network tab, click the page request and read the Content-Security-Policy response header to confirm what the browser received. Chrome's Issues tab groups violations too. Test the pages people actually use, including the sign-in form and any checkout, before you switch from Report-Only to enforcing.
What do AI tools get wrong about CSP?
Ask an assistant why a script is blocked and it will often suggest adding 'unsafe-inline', or 'unsafe-eval', or a wildcard like script-src *. Each of those makes the error disappear by turning the protection off. The better answer is nearly always moving the code into a file or adding a per-response nonce.
Assistants also mix up old and new syntax, put frame-ancestors in a meta tag, or write a nonce as a fixed string in a template. When you get a policy from an AI tool, read each directive, look for the word unsafe, and run the page with the Console open. If an attack still works in the lab with the policy in place, the policy is decoration.
Field notes
- A Content Security Policy tells the browser which sources of script and other content to trust.
'unsafe-inline'lets injected inline script run, which undoes most of the protection.- Move inline handlers like
onclick=into a.jsfile or allow them with a nonce. - Report-Only mode logs what a policy would block without breaking the page.
Easy to get wrong
'unsafe-inline' lets an attacker's inline handler run alongside yours, which is the case the policy exists to stop.
A fixed nonce is visible in the page source, so an attacker can copy it into their own script tag.
Report-Only never blocks anything. It is a testing step.
Browsers ignore frame-ancestors in a meta tag, so the page can still be framed.
Questions people ask
Does a Content Security Policy replace escaping?
No. It limits what an injected script can do in browsers that support the policy. Escaping stops the script getting into the page at all. Use both.
Why does my policy block Google Fonts or a map?
Content from another site must be listed in the right directive, for example style-src and font-src for fonts. The Console message names the directive, so you know which one to extend.
What is 'strict-dynamic'?
It lets a script that you trusted with a nonce load further scripts, which helps with tag managers and bundles. Browsers that understand it ignore host lists, 'self' and 'unsafe-inline' in the same directive, so the nonce becomes the only way in.
Can a policy block inline styles too?
Yes, through style-src. Style injection is less dangerous than script, so many sites allow inline styles at first and tighten later.