Repository navigation
fix(extension): security review fixes for what web pages can see - #32
Merged
Merged
Conversation
Security review finding A-02. The content script passed the page's raw
<html lang> attribute to the popup, which showed it verbatim when the
browser had no name for it, so a page could put any sentence it liked
into Kotiko's popup ("This page is in <attacker text>").
The declared language is now canonicalised with Intl.getCanonicalLocales
(at most 35 characters); anything else counts as no declaration. The
popup shows only a name the browser knows for a language tag and drops
the line when there is none, never the raw string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review finding A-04. The word card's document listeners didn't check isTrusted, so a page's own script could open the card on a swapped word with a synthetic click (or speak it with a synthetic "s"), then ask window.find() about text inside its closed shadow root: romanization, pronunciation, notes and the word in other languages. Every document listener of the card (pointerover, pointerout, pointerdown, click, keydown, contextmenu) now ignores events a script made. The jsdom tests stand in for the learner through a helper that dispatches trusted events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review findings A-03 and C-04 (the same issue from two reviewers). The content script announced itself with a kotiko:handoff CustomEvent on the page's document and tore down on any such event, so any page could switch Kotiko off, and could see it installed even where the learner paused it, switched it off, or on a sensitive site. The manifest's content.css was also in every page, so a page could style its own <kotiko-w> and see Kotiko's rules apply. - The handoff between content-script copies goes through the script world they share (a global the page can't see); a copy left from before an update tears down at its next DOM change, which the new copy causes only on pages it swaps anyway. No event is dispatched. - The swap stylesheet moves to ui/swap-style.js and is added (as a constructed sheet, or a <style> where that isn't supported) with the first swap, and removed when Kotiko is paused or off there. - Old pre-0.3 spans are put back only on pages Kotiko swaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…page
Security review finding A-01 (medium). Kotiko swapped every matching
text node, hidden or not, and a page reads its own DOM: a site could
plant a display:none div of dictionary words and read the learner's
whole word list (meanings, target words and languages) back from the
<kotiko-w> swaps in about four seconds, with nothing on screen.
- Only text the learner can see is swapped: rendered (checkVisibility
with opacity, visibility and content-visibility), its block on screen
or within a screen of it, and at least 2 px each way after every
ancestor's clipping. Blocks are watched by an IntersectionObserver
(no layout reads); text a site adds is measured at once, within 4 ms
a slice, so it's still swapped before it's painted. Hidden text waits
until it shows (an opening menu, a fade-in, scrolling to it). On
big.html (100,000 text nodes) the first swap lands as fast as before
and the rest of the page swaps as it scrolls near.
- One page view shows at most 500 distinct concepts, and at most 3
languages of one concept ("mix within the page"), new rules
max_page_concepts and max_page_langs_per_concept in spec/rules.json,
written for the content scripts to extension/spec/page.js. 500 covers
every 800-word article measured even with a 9,000-word vocabulary
(at most 306), and full Wikipedia articles with 1,000 words (277-552).
A page can still see the swaps it shows; the cap bounds what one view
reveals, however it shows its text.
The reviewers' proofs are now regression tests in test/security/,
which the Playwright config runs with the e2e suite (A-01 to A-04).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
Security review findings A-01, A-03, A-04 and C-04. The policy said sites "can't see the original words, their meanings or your word list", which wasn't true: a page knows its own text, so each swap tells it one of the learner's words. "What websites can see" now says what a site learns from the swaps, the limits Kotiko puts on that (only text on screen, at most 500 different words in one visit, each in at most 3 languages), what remains (a site visited often or reloading itself can learn more over time; an open word card's text can be found with window.find), and that Kotiko adds nothing where it's paused or off. Also: the data inventory's section 2, the assurance case's residual risks and threat model, slice 15's implementation notes, and the changelog. The bundled copy is synced (scripts/sync-privacy.mjs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…-page Signed-off-by: Ayla Croft <dev@scriptkittyos.com> # Conflicts: # CHANGELOG.md # extension/spec/spec.js
The manifest no longer lists content.css (the swap style now comes with the first swap, A-03/C-04), so the insertCSS call for tabs open at an install or update never ran. The comment now describes the handoff as it is: the new copy finds the old one itself, with nothing a page can see. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…s test-hook guard The release build now refuses any `__kotiko` outside a test-only block (A-09), and the content scripts' handoff (A-03/C-04) named its live copy `__kotikoContent`. It lives in Kotiko's own script world, where pages can't see it, so only the name changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
The language a word shows in is drawn per page per day (slice 18). Three new tests in this branch expected the Russian word for "thanks", which the draw picks on some days only; on 2026-10-07 it drew Mandarin, and CI failed. #30 fixed one such test by hand; this pins the page's clock to noon on 2026-10-06 for every test page, with time still running on, so no content test can depend on the day again. Without it, three tests fail under TZ=UTC today; with it, all pass in four time zones covering 6, 7 and 8 October. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
…rops the learner's words Slice 54's A-06 fix keeps what storage.local held only on an update from 0.1 or 0.2, but kept no record that it had decided. A browser update (onInstalled "chrome_update") later in the same worker ran the check again, saw no update from 0.2, and dropped the words, the server and its token, and the first-run state that the update had just kept. The decision is now made once, by the first event after the set-up. Also, on an update the drop now runs before the upgrade steps (onboarding, bases, hidden languages, loopback addresses), so none of them writes something the drop then erases. Both races made two welcome tests time out about one run in four, on main as well. A new test updates from 0.2, then fires a browser update: before the fix the words were gone every time (5/5); after it they stay. The welcome tests pass 40 runs out of 40. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Ayla Croft <dev@scriptkittyos.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Page-facing fixes from the pre-release security review (slice 54, SCR-476). Each fix has a regression test that failed before it, and
test/security/page.spec.mjsnow runs in CI's e2e. Merge last: this branch already contains #31, which contains #29, with their conflicts resolved, so its diff shrinks to its own commits once those are onmain.max_page_conceptsandmax_page_langs_per_concept.langtext. The tag is canonicalised, and an invalid one counts as undeclared.kotiko:handoffevent is gone, so pages can't switch Kotiko off. Paused, off and sensitive pages get nothing from Kotiko, not even its stylesheet, which now arrives with the first swap.extension/spec/spec.js);background.js's deadinsertCSSfor open tabs;Checks on the combined branch: lint; coverage 94.35 % lines and 84.98 % branches;
npm test2,029 passing, 4 runs in a row; perf within budget; Playwright 113 passed; privacy and spec sync; old-name check; DCO.Known limits, recorded as residual risks: the cap is per page view (a page that reloads itself gets 500 more each time), tiny or covered text inside the viewport still counts as visible, and while a card is open,
window.findcan match its text.🤖 Generated with Claude Code