Skip to content

fix(content): the word card opens only for the learner; filter, clip-path and mask count as hidden (review E-04, E-05) - #37

Merged
HackTuah merged 4 commits into
mainfrom
fix/sec-e-page
Oct 7, 2026
Merged

HackTuah merged 4 commits into
mainfrom
fix/sec-e-page

Conversation

@HackTuah

@HackTuah HackTuah commented Oct 7, 2026

Copy link
Copy Markdown
Member

Fixes E-04 and E-05 from reviewer E on v1.0.0-rc.3 (slice 54, SCR-476). Both made the privacy policy's "What websites can see" untrue. Each fix has regression tests in test/security/page.spec.mjs and the DOM suites, and the tests failed on the old code.

E-05: only the learner opens a word's card. On rc.3, a page could restyle one of its swapped words as a nearly transparent, full-screen element. A resting mouse, or a click on the page's own button, then opened Kotiko's card, and the page read it with window.find (the lead reproduced cardOpen: true on hover alone). Now every way of opening the card (hover, click, tap, long press, Enter) first checks that the word is really there:

  • it is rendered and at least 10 % opaque (ancestors' opacity and filter: opacity() included);
  • no clip-path, mask or url() filter covers it;
  • its box is not stretched far beyond its text;
  • for a pointer, the event lands within 4 px of the word's own drawn text (Range.getClientRects()).

Enter on a focused word still works.

E-04: text hidden by filter, clip-path or mask isn't swapped. Text below 10 % opacity, or under any clip-path, mask or url() filter, waits until it's actually shown. That is a conservative rule. All five variants from E's proofs now reveal 0 of 300 words; each revealed 300 before. The style lookups are cached per slice, and the first swap on a 100,000-node page is unchanged at about 700 ms.

Privacy policy, version 5: it now says exactly which hidden text is never swapped, what still counts as shown (tiny, background-coloured, blurred or covered text), and when a card opens, including the remaining case of a word disguised in the background colour.

Lead check: test/security/page.spec.mjs 20/20 on this branch.
Checks: lint; coverage 94.41 % lines, 85.07 % branches; perf; Playwright 125 passed; npm test 2072 pass ×3; spec and privacy sync; old-name; base-neutral; REUSE; DCO.

🤖 Generated with Claude Code

Ayla Croft and others added 3 commits October 7, 2026 15:47
Security review finding E-05 (slice 54, reviewer E). A swapped word sits in
the page's own DOM, so a page could restyle it to cover the whole screen
almost invisibly. The learner's pointer resting anywhere, or a click on one
of the page's own buttons, then landed on the word and opened its card,
and the page searched the card's text with window.find.

The card now opens only on a word the learner can see: rendered, at least
10 % opaque counting every ancestor's opacity and filter: opacity(), under
no clip-path, mask or url() filter, and with a box no more than about twice
the size of its text. A hover, click, tap or long press must also land
within 4 px of the word's drawn text. Enter on a focused word and the
"Show details" command still open it. Kotiko's own text nodes count as the
word's text; elements a page puts inside the word don't.

Regression tests: test/security/page.spec.mjs (A-04b resting pointer, a
click on a page button under a stretched word, a transparent word kept
under the pointer, A-04c reading within one frame, and a real hover and
click still opening) and test/dom/popover.test.mjs. Each failed before
this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review finding E-04 (slice 54, reviewer E). checkVisibility()
doesn't see text hidden with filter: opacity(0), a clip-path that clips
everything or a fully transparent mask, so Kotiko swapped all of it and a
page could read the learner's words there: 300 of 300 in reviewer E's test,
in Chromium and Firefox.

Text is now swapped only when it is at least 10 % opaque, counting every
ancestor's opacity and filter: opacity(), and under no clip-path, mask or
url() filter. A clip-path's shape can't be measured cheaply and a mask's
image can't be read at all, so text under any of them waits, even where
it would show; it is looked at again after a transition, an animation, or
the learner's click, key or scroll, as hidden text already was. Each
element's style is read once per 8 ms slice. Measured in Chromium: about
0.7 microseconds more per text node on the 100,000-node page and 7 on a
page 30 elements deep, with no change in the first swap or the longest task
on either page. No text in the e2e corpus pages is affected.

Regression tests: test/security/page.spec.mjs (filter: opacity(0),
clip-path inset(100%) and circle(0), a transparent mask, opacity 0.01:
0 of 300 words each; the same probe in plain view swaps, and swaps once
the clip-path goes) and test/dom/content.test.mjs. Each failed before this
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…rd opens

For security review findings E-04 and E-05 (slice 54, reviewer E). The
privacy policy moves to version 5. "What websites can see" now lists the
hidden text Kotiko never swaps: text not displayed, see-through or nearly
so (a filter included), clipped or masked. It also says what still counts
as shown: very small, background-coloured, blurred or covered text. And it
says the card opens only from the learner's own input on a word they can
see, where it's drawn. A page that disguises a word under a resting pointer
could still get its card opened. The inventory, the assurance case's
residual risks and the changelog say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
@HackTuah
HackTuah requested a review from minitru as a code owner October 7, 2026 19:54
@HackTuah
HackTuah merged commit 2304bd6 into main Oct 7, 2026
14 checks passed
@HackTuah
HackTuah deleted the fix/sec-e-page branch October 7, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant