Write a policy
Why it matters A policy that is too loose lets the attack through; a policy that is too tight breaks your own page, and then someone switches CSP off "for now". The skill is writing the tightest policy that still lets your page work. You will do it in code, so it works for any page, and a checker will grade both halves.
Back to the guest list. A good host does not write "anyone in a nice coat". They look at who is actually invited and write those names, and nobody else. Here you do the same: look at what the page really loads, and allow exactly that.
The attack to stop
In Module 10 an attacker's text on a practice page carried script: a <script> block in the guestbook (Lesson 58), an <img onerror> that slipped past the toy filter (Lesson 61), and pretend code that sent cookies to prize.example (Lesson 62). Escaping fixed those. Here you add the second line, in case one spot is ever missed. Injected code usually arrives in one of three shapes, and the practice pages here contain all three:
- an inline
<script>...</script>block; - an inline event handler, an attribute such as
onerror="..."on an image tag; - a
<script src="https://prize.example/x.js">pointing at the attacker's server.
A good policy blocks all three, and still lets the page load its own app script, its style sheet, its images, its payment library, its video frame and its one inline script.
Inline event handler: script written straight into an HTML attribute whose name starts with on, such as onerror or onclick. The browser runs it when that event happens. CSP treats it as inline script.
The practice page and the checker
practice_site(seed) from watched.leg3a_csp returns a page as a dict: its origin and a list of resources. Each resource has a kind (script, style, img, font, connect, frame), either a url or inline: True, and injected, which is True for the attacker's three. The page's own inline script carries a nonce: a random value only the real server knows (Lesson 66 explains it fully).
{"kind": "script", "url": "https://pay.example/lib.js", "injected": False}
{"kind": "script", "inline": True, "nonce": "y334r23xgecgnryn", "injected": False}
{"kind": "script", "inline": True, "handler": True, "injected": True}evaluate(policy, page) plays the browser. It returns broken (needed resources your policy blocks) and attacks_run (injected ones it lets through). You want both empty. print_report(policy, page) from watched.leg3d_csp_more prints the same thing readably.
A plan
- Start every directive with
'self', so your own files load. - For each needed outside URL, add its scheme and host, such as
https://pay.example, to the directive for its kind. Nothttps:and not*: those letprize.examplein too. - For the page's own inline script, add
'nonce-VALUE'toscript-src. Never'unsafe-inline'. - End with
default-src 'self'for everything else, plusobject-src 'none',base-uri 'none'andframe-ancestors 'none', which cost nothing on a page that needs none of them.
Notice what this does to the event handler: no source you can add allows onerror= attributes except 'unsafe-inline', so leaving that keyword out blocks them all. Notice too that each kind gets its own list. If your page fetches data from api.example, that host belongs in connect-src, not script-src; otherwise an attacker who can upload a file there could run it as a script.
A common misconception: "script-src https: is strict because it forces HTTPS". It only forces the scheme. Any attacker can host a script on an HTTPS address.
In other languages
Once you have the header, every server sets it the same way:
// JavaScript (Express)
app.use((req, res, next) => {
res.set("Content-Security-Policy", "default-src 'self'; object-src 'none'; base-uri 'none'");
next();
});// Go (net/http)
w.Header().Set("Content-Security-Policy", "default-src 'self'; object-src 'none'; base-uri 'none'")// PHP
header("Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'none'");# nginx
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'" always;In Django, the django-csp package builds the header from settings and adds a fresh nonce to each request, which is how this site does it.
Recap
- Allow exactly the hosts each kind of resource needs; never
*orhttps:for scripts. - Allow your own inline script with a nonce; leaving out
'unsafe-inline'blocks injected blocks and event handlers. - Add
object-src 'none',base-uriandframe-ancestors, which do not fall back. - Check both halves: nothing needed broken, nothing injected run.
Write two functions:
source_for(url): return the scheme and host ofurl, such as"https://cdn.example"for"https://cdn.example/v3.js?x=1"(no path, query or fragment).write_policy(page): return a CSP header string for a page frompractice_site. Skip resources withinjectedset. For every other resource, useDIRECTIVE_FORto find its directive; each directive starts with'self'and gets'nonce-VALUE'for an inline script, orsource_for(url)for a URL on another origin (no duplicates). Join with"; ", starting withdefault-src 'self'and ending withobject-src 'none',base-uri 'none'andframe-ancestors 'none'. Do not use'unsafe-inline',*orhttps:.
practice_site(seed=0). The tests try 20 different practice pages and a hand-made one, and check both that nothing needed is blocked and that no injected script runs.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.