Leg 3 project: harden the practice shop
Why it matters This project puts the whole of Leg 3 to work on one site. ShopD Kitchen leaks in all the usual ways: trackers, a third-party font, unpinned scripts, weak cookies, no security headers, a greedy sign-up form and data kept forever. You will fix it in code, without breaking the shop, and a scorecard will grade the result from F to A.
Think of it as a home inspection before you sell a house. The inspector has a checklist. Your job is to tick every box without knocking down any walls the family still needs.
Scorecard: an automatic checklist that visits the shop in the practice world, awards points for each privacy check it passes, and turns the total into a grade from F to A.
The practice shop
The shop is one Python dict, a shop config, from practice_shop() in watched.leg3b_scorecard. Print it to read it. Its parts:
"embeds": a list of dicts with"url","kind"("script","font"or"iframe") and"purpose"("ads","analytics","font","library","payment"or"video"). A URL starting with/is on the shop's own site. An embed may also have"integrity"and"load": "click"."headers": the response headers the shop's pages send."cookies": the shop'sSet-Cookielines."signup":"fields"on the form and"needed", the fields the shop really uses."features_off": browser features the shop never uses."retention_days": kind of data to days kept, whereNonemeans forever.
scorecard(shop) builds the shop in the practice world, visits it with a practice browser, and returns a dict with "score" (out of 100), "grade" and the list of "checks". print_scorecard(shop) prints it with advice for every failed check. Anything under "broken" means you removed something the shop needs, and the grade is F however many points you earned.
The checklist (each item is a lesson you have done)
| Check | Points | What to do |
|---|---|---|
| No ad or analytics trackers load | 20 | Drop embeds whose purpose is "ads" or "analytics" (Lessons 70 and 71) |
| Fonts are self-hosted | 10 | Change a font URL to /static/fonts/ plus its file name (Lesson 72) |
| Third-party scripts have SRI | 15 | Add "integrity": sha384 of practice_body(url) (Lesson 73) |
| Video waits for a click | 10 | Keep the video, add "load": "click" (Lesson 72) |
| Cookies are Secure, HttpOnly, SameSite | 10 | Rebuild each line as name=value; Path=/; Secure; HttpOnly; SameSite=Lax (Lesson 62) |
| Referrer-Policy | 5 | strict-origin-when-cross-origin or stricter (Lesson 75) |
| Permissions-Policy | 10 | feature=() for every entry in "features_off", joined with ", " (Lesson 74) |
| Content-Security-Policy | 5 | A policy with frame-ancestors and no 'unsafe-inline' in script-src (Lessons 65 and 68) |
| Sign-up asks only what is needed | 10 | Keep only the fields listed in "needed" (Lesson 76) |
| Every kind of data has a deletion date | 5 | Replace None, and anything over 365, with a number of days from 1 to 365 (Lesson 79) |
A grade of A needs 90 points or more and nothing broken. The payment script must stay (with SRI), and the video must stay (as click-to-load).
Two notes on the checklist. The CSP check refuses 'unsafe-inline' in script-src even next to a nonce: that is the rule you followed in Lesson 65, and this shop has no very old browsers to support. And Module 10's escaping is not on the scorecard only because a shop config has no templates to check; on a real site it comes first, and CSP is the backup.
Changing a copy, not the original
Your harden(shop) function must not change the dict it is given. Start with shop = copy.deepcopy(shop), which copies the dict and everything inside it, and change the copy.
A common misconception: "the safest site embeds nothing". The scorecard marks that as broken, and rightly: a shop nobody can pay in is not a privacy win, it is a closed shop. Good privacy work keeps the features and removes the watching.
In other languages
The same hardening on a real site, in a few frameworks.
# Django settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# Permissions-Policy: a small middleware (lesson 74); CSP: django-csp (module 11)// JavaScript (Express)
app.use((req, res, next) => {
res.set("Referrer-Policy", "strict-origin-when-cross-origin");
res.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
res.set("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'");
next();
});
res.cookie("sid", id, { secure: true, httpOnly: true, sameSite: "lax" });// Go
http.SetCookie(w, &http.Cookie{Name: "sid", Value: id, Path: "/",
Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode})Recap
- Remove trackers, self-host fonts, pin scripts with SRI and put video behind a click.
- Harden cookies and send Referrer-Policy, Permissions-Policy and CSP.
- Collect only needed fields and give every kind of data a deletion date.
- Privacy work keeps the shop working: payment and video stay.
Write harden(shop): take a shop config and return a new hardened config (do not change the one you were given) that scores A on scorecard with nothing broken. Work through the checklist in the lesson. Two helper functions are suggested in the starter: sri_for(url), returning the sha384 SRI value of practice_body(url), and fix_cookie(line), returning the line rebuilt with Path=/; Secure; HttpOnly; SameSite=Lax. Your code must not depend on the exact shop: the tests also run it on other practice shops from shop_variant(seed), with different scripts, cookies, form fields, features and data kinds. Print the scorecard of your hardened shop at the end.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.