Skip to content

fix: everything a full code, security, accessibility and performance audit turned up - #5

Merged
kanywst merged 17 commits into
mainfrom
fix/audit
Oct 24, 2026
Merged

kanywst merged 17 commits into
mainfrom
fix/audit

Conversation

@kanywst

@kanywst kanywst commented Oct 23, 2026

Copy link
Copy Markdown
Member

Fixes everything a thorough audit turned up. The audit had four parts:

  • an independent full-repo code review;
  • an independent security review;
  • axe on desk view, list view and a phone;
  • Lighthouse, mobile and desktop.

Results

Before (production) After (this branch, local)
Lighthouse mobile performance 76 96
Mobile LCP 5.5 s 2.8 s
Lighthouse accessibility 92 96
axe link-name violations 41 0
Unit tests 22 35
e2e tests 29 41

The remaining Lighthouse/axe flag is target-size. It is measured while the desk is zoomed out to the overview, so every element is scaled down. At 100% and in list view it does not fire.

Desk (code review)

  • Click to centre never worked with a mouse. Pointer capture on pointerdown retargeted the click to #desk. Capture now starts with the drag. The new e2e test fails without the fix.
  • List view could be left with a camera transform. A frame still in flight wrote one after switching. List view now halts the camera, and apply() only runs in desk view.
  • Tab escaped the live overlay and flew the camera away. The desk is inert while a site is open.
  • The intro pull-back overrode input in its first 650 ms. Any input cancels it.
  • Safari gesture events zoomed in list view and fought touch pinch. They are now guarded.
  • Board boxes were measured once. A ResizeObserver keeps them current, and board images carry their size from the build.
  • Smaller fixes:
    • An open token prevents a late resume.
    • No negative first frame.
    • The .lifted z-index never took effect and is gone.
    • The unfiled-repo pile no longer sits on the dev.to receipt.
    • New layout tests.

Security review

  • Live frame sandbox. The frame is sandboxed to an opaque origin. Every project site shares this origin on Pages, so a bug in any of them could reach the desk through window.parent. All eight sites render with no errors inside the sandbox.
  • URL allow-lists are checked on parsed URLs instead of string prefixes. github.com.evil, user@ and /0-draft/../ all passed the old check, and each now has a test.
  • API links (homepages, dev.to) are kept only as https URLs.
  • README text decodes numeric references, so guestbook apostrophes no longer show as '.
  • CSP: object-src 'none', no unused data: images, one frame origin.
  • Shots workflow:
    • screenshots are validated chunk by chunk (CRC, IHDR/IEND, size) before commit;
    • the untrusted shoot job no longer reads or seeds the pnpm cache;
    • a run fails only when every attempted shot fails;
    • the image tag follows the installed Playwright.
  • CI: the actionlint image is pinned by digest.
  • Empty responses: sync.ts treats an empty response as a failure, so an org rename cannot empty the desk.

Accessibility and performance

  • Shogi board links. Only cells that open a move issue are links. The other 41 pointed back at the README with no name.
  • Table headers. Tables keep their header row, and a blank header is named for screen readers.
  • Mascot image. The 483 KB PNG at 2x size is now a sized WebP with 1x/2x densities.
  • OGP image. Now a 90 KB JPEG instead of a 790 KB PNG. The unused avatar is removed.

Repo settings (applied separately, not part of this diff)

  • Branch protection on main.
  • Secret scanning with push protection.
  • Dependabot security updates.
  • SHA-pinned actions required.

- A plain click on a sheet never centred it: the pointer was captured on
  pointerdown, which retargets the click to #desk. Capture now starts with the drag.
- Switching to list view mid-flight let the next frame write a transform onto
  the list page. List view now halts the camera, and apply() is desk-only.
- With a site open, Tab walked into the desk and focus-follow flew the camera
  away from the frame. The background is inert while live.
- The intro's pull-back overrode input in its first 650ms and could resume a
  pending open early; any input now cancels it.
- Safari gesture events zoomed in list view and fought touch pinch.
- Item boxes were measured once, so boards that grew after load were cut off
  by fit and the minimap. A ResizeObserver keeps them current.
- An open could resume after Escape and a quick re-press; opens carry a token.
- Escape now also works with focus inside the same-origin frame.
- No negative time step on the first frame.
- Drop the lifted sheet's z-index, which never beat the scrim.

Each of the first four has an e2e test; the click test fails without its fix.
…ibly

- Links and images were allowed by string prefix, so github.com.evil, user@
  hosts and /0-draft/../ paths passed. Both are now checked on parsed URLs.
- Board cells link only when they open a move issue; the rest pointed back at
  the README and left 41 nameless tab stops on the shogi board (axe link-name).
- Tables keep their header row (axe td-has-header).
- sync.ts records each board image's size so boards render at their final size.
- sync.ts treats an empty response as a failure and keeps the previous file,
  so an org rename or token change cannot empty the desk.
…layout

The fallback pile for unfiled repos sat on top of the dev.to receipt, and a
filed repo without a spot landed on its own heading. Tests now require a
heading for every group, a spot for every filed repo, and the unfiled pile
below the rest. The stamp variant is no longer called 'tape'.
The home seal was a 483 KB PNG at twice its displayed size, which put mobile
LCP at 5.5 s. It is now a sized WebP with 1x/2x densities. The OGP image is a
90 KB JPEG instead of a 790 KB PNG, the unused avatar is gone, and so is its
host in the CSP.
…stalled version

Counting kept screenshots as targets let a run with no network go green. The
image tag now follows the installed @playwright/test, as in ci.yml.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e88ac1e1-4830-448f-9d8c-37ef4ffb23a2
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The blackjack table's last header is empty in the README, which left its cells
without a header (Lighthouse td-has-header).
Every project site shares this origin on GitHub Pages, so an unsandboxed frame
gave any of them script access to the desk through window.parent. The frame is
now sandboxed to an opaque origin; all eight sites were checked to render with
no errors inside it. The CSP drops the unused data: images and the duplicate
frame origin, and sets object-src 'none'.

Raised in the security review.
…tities

Homepages and dev.to article URLs reached the page unchecked; they are now kept
only as https URLs (dev.to links only on dev.to). README text decodes numeric
character references, so guestbook apostrophes no longer show as ', and
the image allow-list stores the parsed URL it checked.

Raised in the security review.
…obs off the cache

The propose job checked only a PNG's first 8 bytes before committing it for the
build to decode. scripts/check_png.py (standard library only) now checks CRCs,
IHDR/IEND order, trailing data and size. The shoot job no longer reads or seeds
the shared pnpm cache.

Raised in the security review.
Raised in the security review.
Comment thread src/lib/readme.ts Outdated
Comment thread src/scripts/desk.ts Outdated
Comment thread scripts/sync.ts Outdated
@claude

claude Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review: no blocking findings.

I checked the diff against injection, CSP, the shared desk/list DOM, input and focus, and sync failure modes.

  • Injection: safeHref and safeImg in src/lib/readme.ts now compare parsed origins, which closes the github.com@evil and /0-draft/../ prefix tricks. sync.ts keeps only https: homepages and only dev.to article URLs. Nothing new goes through innerHTML or set:html. In boards.ts, th content is set with textContent, and the w/h values are numeric.
  • CSP: the policy gets tighter. data: and avatars are dropped from img-src, frame-src loses 'self', and object-src 'none' is added. I found no remaining data: or avatar use in src/. The sandboxed frame has no allow-same-origin, and the e2e test asserts that.
  • Two views, one DOM: apply() and halt() are guarded by the desk class. Table headers and the .vh text render without JavaScript. The ResizeObserver only updates boxes, and items are positioned from the top-left, so nothing drifts.
  • Input and focus: pointer capture now starts at the first drag move, so plain clicks keep their targets. release is on window, so a pointer let go outside the desk is still cleared. closeLive removes inert before it refocuses the seal button. openSeq replaces the sheet-identity check, and Escape still works while the seal is cracking.
  • Sync: an empty response or a failed fetch keeps the previous file. Image sizes fall back to the previous games.json.

Not blocking, but worth knowing:

  • The sandbox opaque origin means Escape is not heard while focus is inside the framed site. The Back button and a press on the scrim still close it, and this is documented in desk.ts.
  • Framed sites also lose localStorage and same-origin fetches. Check that the Pages sites you frame don't depend on either.

An out-of-range numeric reference threw from String.fromCodePoint, which failed
the games sync and silently stopped the live board refresh; and numeric
references were decoded before named ones, so text could be decoded twice.

Raised in Claude review on #5.
Image sizes are cosmetic; a failed fetch now leaves that image unsized. Sizes
are fetched in parallel.

Raised in Claude review on #5.
The sandboxed frame keeps its key events to itself, so Escape can't be heard
once focus is inside the site. Pressing the darkened desk now closes it too,
and the close button shows its Esc shortcut.

Raised in Claude review on #5.
Comment thread scripts/sync.ts
Comment thread src/pages/index.astro
Comment thread src/scripts/desk.ts
Hiding the tray, minimap and top bar did not keep Tab out of them in Chromium,
so a zoom or jump could move the camera under the open frame. They are now
inert along with the desk; the e2e test covers it.

Raised in Claude review on #5.
Comment thread src/components/Blocks.astro
@kanywst
kanywst merged commit 0f990d3 into main Oct 24, 2026
8 checks passed
kanywst added a commit that referenced this pull request Oct 24, 2026
An out-of-range numeric reference threw from String.fromCodePoint, which failed
the games sync and silently stopped the live board refresh; and numeric
references were decoded before named ones, so text could be decoded twice.

Raised in Claude review on #5.
kanywst added a commit that referenced this pull request Oct 24, 2026
Image sizes are cosmetic; a failed fetch now leaves that image unsized. Sizes
are fetched in parallel.

Raised in Claude review on #5.
kanywst added a commit that referenced this pull request Oct 24, 2026
The sandboxed frame keeps its key events to itself, so Escape can't be heard
once focus is inside the site. Pressing the darkened desk now closes it too,
and the close button shows its Esc shortcut.

Raised in Claude review on #5.
@kanywst
kanywst deleted the fix/audit branch October 24, 2026 06:13
kanywst added a commit that referenced this pull request Oct 24, 2026
kanywst added a commit that referenced this pull request Oct 24, 2026
Hiding the tray, minimap and top bar did not keep Tab out of them in Chromium,
so a zoom or jump could move the camera under the open frame. They are now
inert along with the desk; the e2e test covers it.

Raised in Claude review on #5.
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