CSP Academy

← The ten steps · Step 4 of 10 · 6 minutes

The directives that matter

There are around twenty directives. Nine of them carry almost all of the value, and they are not the nine most tutorials start with.

The floor

default-src

The fallback for everything the page fetches. Set it to 'self' and you have a sane baseline: anything you forget to name specifically inherits a restriction rather than a hole.

default-src 'self'

The one that actually stops attacks

script-src

Where scripts may come from, and whether inline script and eval are permitted. This is the directive CSP exists for. Everything in steps 5 to 7 is about getting this one right.

Two sub-directives exist and are worth knowing because browsers report them: script-src-elem governs <script> elements, and script-src-attr governs inline event handlers like onclick=. If you set only script-src, both inherit from it.

The four that never inherit

These do not fall back to default-src, because they do not describe a fetch. If you have not written them, they are not set — and unset means unrestricted. This is the most common gap in otherwise good policies.

base-uri

An injected <base href> rewrites where every relative URL on the page resolves, including your own nonced <script src="app.js">. Nothing in script-src stops it.

base-uri 'none'

form-action

A <button formaction> overrides where a form submits. Not a script, not a navigation, not covered by anything else.

form-action 'self'

frame-ancestors

Who may put your page in an iframe. Supersedes X-Frame-Options and, unlike it, can name several origins.

frame-ancestors 'none'

object-src

Governs <object> and <embed>. It does inherit from default-src in current browsers, but it did not always, and there is essentially never a reason not to say it.

object-src 'none'

The three you will tune

style-src

Where CSS may come from and whether inline styles are allowed. Lower stakes than script, but 'unsafe-inline' here still enables CSS-based data exfiltration through attribute selectors — a real technique, just a slower one.

img-src

Where images may load from. Frequently set to * because images feel harmless. They are not entirely: an image URL is an outbound request an attacker can put data into, so a wildcard here is a ready-made exfiltration channel after an injection.

connect-src

Where fetch, XHR, WebSockets, EventSource and sendBeacon may talk to. This is your API allowlist, and it is the directive that decides where data can go once script is already running.

A complete starting policy

default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none';
report-uri <your endpoint>

Ship that in report-only mode, read what it catches, and widen the three tunable directives to fit what you actually load. Do not widen script-src by adding hosts — step 5 explains why, and step 6 gives you the alternative.

You can do this now

Paste your current policy into the analyser. It checks specifically for the four that never inherit, and it is very common to be missing three of them.