Syllabus Lesson 80 of 156 · Privacy by design
Privacy by design

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's Set-Cookie lines.
  • "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, where None means 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)

CheckPointsWhat to do
No ad or analytics trackers load20Drop embeds whose purpose is "ads" or "analytics" (Lessons 70 and 71)
Fonts are self-hosted10Change a font URL to /static/fonts/ plus its file name (Lesson 72)
Third-party scripts have SRI15Add "integrity": sha384 of practice_body(url) (Lesson 73)
Video waits for a click10Keep the video, add "load": "click" (Lesson 72)
Cookies are Secure, HttpOnly, SameSite10Rebuild each line as name=value; Path=/; Secure; HttpOnly; SameSite=Lax (Lesson 62)
Referrer-Policy5strict-origin-when-cross-origin or stricter (Lesson 75)
Permissions-Policy10feature=() for every entry in "features_off", joined with ", " (Lesson 74)
Content-Security-Policy5A policy with frame-ancestors and no 'unsafe-inline' in script-src (Lessons 65 and 68)
Sign-up asks only what is needed10Keep only the fields listed in "needed" (Lesson 76)
Every kind of data has a deletion date5Replace 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.
Your turn

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.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output