Audit a site's third parties
Why it matters You cannot fix what you have not counted. Before changing anything on a site, privacy engineers capture a page load and list every outside server it talked to. In this exercise you write that audit yourself, in a few lines of Python, and you will reuse the idea in the Leg 3 project.
Think of a guest book at the cafe door. At the end of the day you read it and ask: who came in, how often, and did anyone leave something behind? A request log is the website's guest book.
HAR file: short for HTTP Archive. DevTools can save every request a page made into one file (Network tab, right-click, "Save all as HAR"). Each entry lists the URL, the kind of request, its headers, the status and any cookies set. Our practice browser gives you the same thing as a list of dicts with browser.har().
One entry from the practice world looks like this (trimmed):
{"method": "GET",
"url": "https://adco.example/p.gif?u=https%3A%2F%2Fshopb.example%2F",
"kind": "beacon",
"top_site": "shopb.example", # the site in the address bar
"status": 200,
"set_cookies": ["adco_id=9f3c; Max-Age=31536000; Secure; SameSite=None"]}Real HAR files do not have top_site; you would read it from the page URL. Our log adds it to keep the code short.
Three questions an audit answers
- Is this request to a third party? Compare the site of the URL with
top_site. Use the site, not the full host:img.shopb.examplebelongs toshopb.example, so it is first party. - How much does each outside host get? Count its requests and note whether it set cookies.
- Who sees people across sites? A host that shows up under two or more different
top_sitevalues can link visits. Those are the ones to look at first.
Two helpers from the practice world do the URL work for you: host_of(url) gives the host ("img.shopb.example"), and site_of(url) gives the registrable site ("shopb.example").
from watched import host_of, site_of
url = "https://img.shopb.example/logo.png"
print(host_of(url)) # img.shopb.example
print(site_of(url)) # shopb.exampleTo build a count per host, the dict method setdefault(key, default) is handy: it returns the value for key, first storing default if the key is new.
counts = {}
for word in ["a", "b", "a"]:
row = counts.setdefault(word, {"n": 0})
row["n"] += 1
print(counts) # {'a': {'n': 2}, 'b': {'n': 1}}A common misconception: "a host that sets no cookies is harmless". A host that appears on every site still sees the IP address and browser each time, and can pass its own ID in the URL instead of a cookie. Count requests and sites, not only cookies.
In other languages
The same audit over a HAR file exported from DevTools. Real HAR files keep entries under log.entries.
// JavaScript (Node)
const har = JSON.parse(fs.readFileSync("page.har", "utf8"));
const report = {};
for (const e of har.log.entries) {
const host = new URL(e.request.url).hostname;
report[host] = (report[host] || 0) + 1;
}
console.table(report);// Go
for _, e := range har.Log.Entries {
u, _ := url.Parse(e.Request.URL)
counts[u.Hostname()]++
}// C#
var counts = har.Log.Entries
.GroupBy(e => new Uri(e.Request.Url).Host)
.ToDictionary(g => g.Key, g => g.Count());Getting the site right needs the Public Suffix List (so a.co.uk is not treated as part of co.uk). Use a library such as tldts in JavaScript or golang.org/x/net/publicsuffix in Go rather than splitting on dots.
Recap
- A request log (HAR) is a site's guest book: every outside server a page load contacted.
- Third party means a different site from the address bar, not just a different host name.
- Hosts seen under several different sites are the ones able to link visits.
The starter code captures a practice log for you: Sam browses 12 shop pages while AdCo sets an ID cookie. Write three functions:
is_third_party(entry): returnTruewhensite_of(entry["url"])is different fromentry["top_site"], otherwiseFalse.audit(har): go through the list and, for every third-party entry, build a dict keyed byhost_of(entry["url"]). Each value is a dict with"requests"(how many entries went to that host),"sites"(a sorted list of the differenttop_sitevalues it was seen under) and"sets_cookies"(Trueif any of its entries has a non-emptyset_cookieslist). Return the dict. First-party entries are left out.watchers(report): given the dict fromaudit, return a list of the hosts seen under two or more sites, sorted alphabetically.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.