Skip to content

Refuse a page that sets a cookie, reaches storage or carries a handler [#50] - #165

Merged
iderex merged 1 commit into
mainfrom
the-static-half-of-the-cookie-gate
Aug 16, 2026
Merged

Refuse a page that sets a cookie, reaches storage or carries a handler [#50]#165
iderex merged 1 commit into
mainfrom
the-static-half-of-the-cookie-gate

Conversation

@iderex

@iderex iderex commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The privacy page made three promises about what a page here does to a reader's
browser and nothing refused a page breaking any of them. The page said so
itself, which is that register working as intended and is also the state this
change is about:

git show origin/main:content/privacy.txt | grep -c '^promised:'
3
git show origin/main:internal/invariant/invariant.go | grep -c -E 'page-touches-no-browser-storage|page-carries-no-inline-handler'
0

Both run 2026-08-16 against 72e7f2d.

One of the three named the gap in its own sentence: what was refused was a page
fetching a script, and a handler written into the page was refused by nothing.
That is a real hole rather than a tidy one. The row about a script element reads
its src attribute, so a page with no source anywhere and one handler on one
button runs code in a reader's browser and passes every row this gate had.

What this does

Two rows over every produced page, which is the readable half of #50.

page-touches-no-browser-storage refuses a meta element that sets a cookie, and
refuses any name a page would have to spell to reach a cookie, either browser
storage area or a reporting call. The two halves are separated in the refusal
because the repair is not the same. A meta element is a header this site chose
to write into the document, which a host serves verbatim and a browser acts on
with nothing running at all. A name is code that would have to run. A refusal
naming only one of the two sends the next person looking in the wrong half of
the page.

page-carries-no-inline-handler refuses an event handler attribute on an
element, and an address whose scheme is a script. An address of that shape
carries no attribute name a handler pattern would find, and the row that reads
what a page references reads a host, which such an address does not have, so it
is refused here rather than by either of its neighbours.

The privacy page now carries two of its promises as checked statements naming
these rows. What is still not covered stays written down and gets sharper rather
than quieter. A script element carrying its code inside the page is refused by
nothing, and both rows read the names a page would have to spell rather than
what it does, so a page reaching the same interface through a value none of
those spellings finds is refused by nothing here. Both are promises on the page
with the issue that would turn them into properties, and the second is a bound
the rows print in their own comments as well.

Closes

Nothing. It does not close #50, and the reason is the other leg that issue asks
for rather than an omission here.

#50 asks for two legs and this is one of them. The other loads every produced
page in a headless browser, asserts that after load there is no cookie, nothing
in either storage area and no request that left the origin, and reds when the
browser cannot start. There is no browser in this tree to run it with:

ls harness/
needs-network
README.md

Run 2026-08-16 against 72e7f2d. Pinning that browser is #63. So the clause
about a page with a beacon redding the browser leg, the clause about zero
cross-origin requests after load and the clause about a browser that fails to
start are all untouched by this change, and the issue stays open on them.

What was run

go run . ci
gate: 7 legs, in order: format, vet, test, build, links, sitemap, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 50 file(s)
  vet: ok
  test: ok, 24 test file(s)
  build: ok, 7 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  sitemap: every page the build wrote is listed once, and every entry has a page behind it
  invariants: ok, 36 rule(s) decided, 1 owed and not decided
7 of 7 legs ran. None was skipped.

The needs-network set was not asked for, which the gate prints above rather
than this body leaving it out. Nothing in this change opens a window, needs a
display, binds anything, reaches the network or asks for elevation, and no test
here is skipped for any reason.

What the two rows report over the pages this build writes:

go run . invariants | grep -E '^  page-(touches-no-browser-storage|carries-no-inline-handler)'
  page-touches-no-browser-storage: ok, 4 file(s) of every page the build produced
  page-carries-no-inline-handler: ok, 4 file(s) of every page the build produced

And what the privacy page says now, which is two statements moved out of the
promised register into the checked one:

go run . build | grep privacy
wrote dist/privacy/index.html (4945 bytes, 4 checked, 3 promised, 2 residual)
grep -o -E 'data-refused-by="[^"]*"' dist/privacy/index.html
data-refused-by="page-fetches-no-script"
data-refused-by="output-references-no-domain-outside-the-allowlist"
data-refused-by="page-touches-no-browser-storage"
data-refused-by="page-carries-no-inline-handler"

All three run 2026-08-16 against 72e7f2d.

Each row is shown to bite by switching it off and watching the suite go red.
With decideBrowserStorage made to refuse nothing:

go test ./internal/invariant/
--- FAIL: TestEveryRowRefusesItsOwnViolationAndPassesTheNeighbour (0.01s)
--- FAIL: TestRunRefusesAFrameThatSetsACookie (0.20s)
--- FAIL: TestRunRefusesAPageThatNamesAStorageInterface (0.21s)
FAIL	github.com/Flowfin/site/internal/invariant	7.154s

With decideInlineHandler made to refuse nothing:

go test ./internal/invariant/
--- FAIL: TestEveryRowRefusesItsOwnViolationAndPassesTheNeighbour (0.00s)
--- FAIL: TestRunRefusesAPageCarryingAHandlerRatherThanAScriptSource (0.37s)
--- FAIL: TestRunRefusesAnAddressWhoseSchemeIsAScript (0.50s)
FAIL	github.com/Flowfin/site/internal/invariant	8.635s

With both in place:

go test ./internal/invariant/
ok  	github.com/Flowfin/site/internal/invariant	6.577s

All three run 2026-08-16 against 72e7f2d with the stated edit applied and
reverted.

The near miss for the second row is the one somebody actually writes rather than
one that could not have failed. Its fixture is a script element carrying its
code inside the page, so the older row about a script source passes it and the
case asserts that it does:

go test ./internal/invariant/ -run TestRunRefusesAPageCarryingAHandlerRatherThanAScriptSource
ok  	github.com/Flowfin/site/internal/invariant	1.119s

go run . hygiene origin/main HEAD
hygiene: 1 non-merge commit(s) in origin/main..HEAD, origin internal
  72e7f2d27816: subject carries its reference
1 commit(s) judged, none refused.

Both run 2026-08-16 against 72e7f2d.

The means

Go, as two rows in the table the invariant verb already reads, and no language,
runtime or dependency the tree does not carry. It fits for the reason that table
exists: a rule written as a shell block in a workflow has no suite that can
prove it bites and no way to run it on the machine where the mistake was made,
and both rows here are proved by deletion above. Adding a row is adding a row,
so the case that requires one violation fixture per row picked both of these up
without being asked.

The alternative was the browser, and it is not an alternative for these two. A
browser decides what a page does; these read what the build wrote, which is the
half that can be decided with no network and no browser and therefore the half
that belongs in the gate rather than behind a pin that does not exist yet.

Who read it

Nobody but me. This board has no second reader tonight and the ruleset requires
no approving review, so the evidence above stands in place of one rather than a
merge being read as a review.

#50]

The privacy page promised that no cookie is set, that nothing is written into
either browser storage area, and that no page needs scripting to be read.
Nothing refused a page breaking any of the three, and the page said so on
itself: the promise about scripting named the gap in its own sentence, that
what was refused was a page fetching a script and a handler written into the
page was refused by nothing.

Two rows over every produced page close the readable half of that.

The first refuses a meta element that sets a cookie and refuses any name a page
would have to spell to reach a cookie, either browser storage area or a
reporting call. The two halves are separated in the refusal because the repair
is not the same. A meta element is a header this site chose to write into the
document, which a host serves verbatim and a browser acts on with nothing
running at all; a name is code that would have to run, and a refusal naming
only one of the two sends the next person looking in the wrong half of the
page.

The second refuses an event handler attribute on an element and an address
whose scheme is a script. That is the near miss the older row does not catch:
it reads a script element's src attribute, so a page with no source anywhere
and one handler on one button runs code in a reader's browser and passes every
row this gate had. The fixture for it is a script element carrying its code
inside the page, which is the shape that arrives without anybody thinking they
have added a request.

The privacy page now carries two of its promises as checked statements naming
these rows, and the register keeps the shape it had. What is not covered stays
written down and gets sharper rather than quieter: a script element with its
code inside the page is still refused by nothing, and both rows read the names
a page would have to spell rather than what it does, so a page reaching the
same interface through a value none of those spellings finds is refused by
nothing here. Both are promises on the page with the issue that would turn them
into properties.

Each row is shown to bite. With either one made to refuse nothing, three cases
go red; with both in place the suite passes.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex self-assigned this Aug 16, 2026
@iderex iderex added this to the Legal and the data promise milestone Aug 16, 2026
@iderex
iderex merged commit 8af1685 into main Aug 16, 2026
17 checks passed
@iderex
iderex deleted the the-static-half-of-the-cookie-gate branch August 16, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refuse a cookie, a storage write or a beacon in the output

1 participant