Syllabus Lesson 121 of 156 · Build the defences
Build the defences

Storage Access rules

Why it matters Partitioning stops trackers, but it also logs you out of embedded things you actually want: a comments box, a video you pay for, a sign-in button. If the engine offers no way back, people turn the protection off. The Storage Access API is that way back, and its rules decide whether it helps you or helps a tracker. In this lab you write those rules.

Think of a library card. You signed up at the library, so the card works there. Now you are at a friend's house, and their house rule is that outside cards do not work indoors. You can still use your card there if you hand it over on purpose, the library is one you really joined, and the permission is for this house only, for a while.

In the practice world, talk.example is a comments service. Sam signs in on talk.example itself. Its widget is embedded on news.example. With partitioning on, the widget on the news site gets a fresh, empty cookie jar, so it shows "sign in to post" even though Sam is signed in.

Embedded frame (iframe): a page shown inside another page, like a window in a wall. The comments widget is a frame from talk.example inside news.example.

User gesture: a real click, tap or key press by the person, a moment ago. Browsers call it user activation. A script running on its own cannot fake one.

Grant: the browser's note that one embedded site may use its own cookies inside one top-level site, until an expiry time.

How the real API looks

Inside the frame, a script can ask document.hasStorageAccess() (do I have my cookies?) and, after a click, document.requestStorageAccess() (please give them to me). The browser checks its rules, may show the person a prompt, and then answers yes or no. The API ships in Safari, Firefox and Chrome (Chrome since version 119). In Chrome it matters only for people who block third-party cookies, because Chrome still sends them by default, and the Related Website Sets feature that once granted access automatically has been retired (checked October 2026). The lesson "The Storage Access API" in the previous module covered how engines differ; here you build the core rules.

Our engine's rules

Real engines differ in the details (prompts, time limits, extra checks). Our practice engine uses four plain rules, checked in order, which capture the ideas they share:

  1. If the frame's site is the top-level site, say yes: it already has its own storage.
  2. If there was no user gesture, say no. A widget may not ask silently as the page loads.
  3. If the person has never opened the frame's site directly (as a top-level page), say no. This stops an advert frame you never chose from getting access just because you clicked it.
  4. Otherwise say yes, and record a grant for exactly this pair, (frame_site, top_site), that expires GRANT_SECONDS (30 days) from now.

Checking access later is simpler: same site means yes; otherwise look up the pair in the grants and say yes only if it is there and has not expired yet.

Pieces of Python you will use

grants = {}                                     # an empty dict
grants[("talk.example", "news.example")] = 5000 # key is a tuple, value an expiry time
expiry = grants.get(("talk.example", "blog.example"))
print(expiry)                                   # None: .get gives None when the key is missing
print(expiry is not None and 4000 < 5000)       # False, because expiry is None

x is not None asks "is there a value at all?". Putting it first in an and matters: if it is False, Python stops and never compares None with a number, which would be an error.

Time in the practice world is a count of seconds (world.now), so "30 days from now" is just now + 30 * 24 * 3600, which the module gives you as GRANT_SECONDS.

The story your rules must pass

widget_story(request_fn, access_fn) plays six steps on the practice AccessBrowser and returns a list of (step, result) pairs:

StepShould the widget be signed in?
1. Sam opens news.example (signed in on talk.example earlier)No: partitioned
2. The widget asks as the page loads, with no clickNo: no gesture
3. Sam clicks "Sign in to comment"Yes: granted for this pair
4. Sam opens recipes.example with the same widgetNo: the grant was for news.example only
5. AdCo's advert frame asks after a click on the advertNo: Sam never opened adco.example
6. 31 days later, news.example againNo: the grant expired

The module also keeps EXPECTED_STORY, the list of right answers, so you can compare as you go.

A common misconception

"Once a widget gets access, it can follow me everywhere." Not with these rules. The grant is for one pair of sites. The same widget on another site starts with an empty jar again, and the grant runs out. That pair rule is what keeps the API from becoming a new way to track.

In other languages

The widget side, in the JavaScript a real comments widget would run:

// Inside the talk.example frame, on news.example
signInButton.addEventListener("click", async () => {      // a real click: a user gesture
  if (!(await document.hasStorageAccess())) {
    try {
      await document.requestStorageAccess();                // the browser applies its rules
    } catch {
      showMessage("Open talk.example to sign in first");   // refused
      return;
    }
  }
  loadComments();   // requests now carry talk.example's own cookie
});

The engine side, the access check, in Rust:

use std::collections::HashMap;
fn has_storage_access(grants: &HashMap<(String, String), u64>,
                      frame_site: &str, top_site: &str, now: u64) -> bool {
    if frame_site == top_site { return true; }
    match grants.get(&(frame_site.to_string(), top_site.to_string())) {
        Some(expiry) => now < *expiry,
        None => false,
    }
}

Recap

  • Partitioning gives embedded frames an empty jar; the Storage Access API lets a wanted widget ask for its own cookies back.
  • Good rules need a real click, a site the person really used, and a grant for one pair of sites that expires.
  • The pair rule is what stops the API from becoming a tracking tool.
  • The API ships in Safari, Firefox and Chrome (Chrome 119), checked October 2026.
Your turn

Write request_storage_access(grants, frame_site, top_site, user_gesture, visited_before, now) using the four rules in order: same site returns True; no user_gesture returns False; not visited_before returns False; otherwise set grants[(frame_site, top_site)] = now + GRANT_SECONDS and return True. Write has_storage_access(grants, frame_site, top_site, now): True for the same site, otherwise True only when the pair is in grants and now is before its expiry. Then set story = widget_story(request_storage_access, has_storage_access).

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output