Skip to content

Keep the page's security policy on: load the virtual screen reader as a plain script (0.13.0) - #48

Merged
Elizabeth1979 merged 1 commit into
mainfrom
vsr-plain-script
Oct 17, 2026
Merged

Elizabeth1979 merged 1 commit into
mainfrom
vsr-plain-script

Conversation

@Elizabeth1979

Copy link
Copy Markdown
Owner

What this means for users

A page can tell the browser which scripts it may run (its Content-Security-Policy). Until now, every virtual screen reader command (audit, page, nav, speak, repl, screenshot, the per-finding announcements in scan, the dashboard and the MCP server) asked the browser to ignore that policy, Playwright's bypassCSP: true. On a locked-down site the tool switched the lock off.

Now the lock stays on, and the reader still works. Example: a page served with default-src 'none'; script-src 'none'.

Old loader, lock on This PR
audit Failed to fetch dynamically imported module: blob:… main · heading, Locked-down page, level 1 · the paragraph · button
scan finding for the nameless button no announcement screen reader says "button"
audit --record overlay highlight and caption lose their styles drawn as normal

How: the way Deque's @axe-core/playwright loads axe

  • scripts/build-virtual-screen-reader.mjs bundles Guidepup's browser build into one plain script with esbuild (format: "iife", global __srCliGuidepupVirtualScreenReader). It runs on npm install and before npm publish (prepare), and ships as dist/guidepup-virtual-screen-reader.js with Guidepup's MIT licence beside it (dist/guidepup-virtual-screen-reader.LICENSE). dist/ is in the files allowlist, and npm pack --dry-run lists both files.
  • injectVirtualScreenReader in src/bridge.js is the one loader every command uses. It evaluates the plain script with page.evaluate(source) over the DevTools protocol, which a page's policy does not govern. That is exactly how Deque's official @axe-core/playwright injects axe (page.evaluate(this.script())), and how runAxe here already loaded axe. It is the same approach accessibility-evidence-engine took (Speak through Guidepup's virtual screen reader instead of our own rules accessibility-evidence-engine#123).
  • Playwright's CSP bypass is removed from bridge.js, services/scanner.js and commands/scan.js (including --chrome-profile), and from the README, CLAUDE.md and DEVLOG. A test keeps it out of the whole repo.
  • Found along the way: the --record overlay used inline style="…" markup, which a strict policy drops. It now sets the same styles from script.

Pinned: @guidepup/virtual-screen-reader exactly 0.32.1

0.33.0's virtual.start() opens a persistent user-event session that can break later typing in a React input (guidepup/virtual-screen-reader#250), and this CLI reads people's real pages. I checked the built bundle: in 0.32.1, start() only sets the container, a DOM observer and a focus listener. The bundle never calls .setup( anywhere, and it only uses the one-shot click / keyboard / pointer / type calls. A test guards this. The 0.33 header-in-a-card fix is already in 0.32.1: audit — header landmarks passes.

Tests

  • New test/csp.test.js: a local HTTP server sends Content-Security-Policy: default-src 'none'; script-src 'none'. The test checks that audit reads the page, that scan attaches "button" to the button-name finding and that the --record overlay keeps its styles. It also checks that nothing in the repo uses the CSP bypass, that the shipped script is 0.32.1 and has no .setup( call, and that the licence ships beside it.
  • The old approach fails this test. With only the bypass lines removed, audit dies with Failed to fetch dynamically imported module and the scan announcement goes missing. With the old inline-style overlay, the caption bar loses its styles.
  • npm test: 170/170 pass.
  • node bin/cli.js scan docs/index.html --fail-on minor exits 0.

Release

Bumps the version to 0.13.0. Merging this publishes 0.13.0 to npm through publish.yml. DEVLOG has a dated entry (2026-10-17). Not merged: that is the owner's call.

🤖 Generated with Claude Code

… a plain script

Every virtual screen reader command asked Playwright to ignore the page's
Content-Security-Policy, because Guidepup's reader was imported as an ES
module from a blob URL and a strict policy refuses that. On a locked-down
site the tool switched the lock off.

Guidepup's browser build is now bundled into one plain IIFE script with
esbuild at install/publish time (scripts/build-virtual-screen-reader.mjs,
run by `prepare`) and shipped as dist/guidepup-virtual-screen-reader.js
with Guidepup's MIT licence beside it. injectVirtualScreenReader, the one
loader every command uses, evaluates it with page.evaluate(source) over the
DevTools protocol, which a page's CSP does not govern. This is how Deque's
official @axe-core/playwright loads axe (page.evaluate(this.script())), and
how runAxe here already loaded axe. The CSP bypass is removed everywhere.

The --record overlay set its styles with inline style="" markup, which a
strict policy drops; it now sets them from script.

@guidepup/virtual-screen-reader is pinned to exactly 0.32.1: 0.33.0's
start() opens a persistent user-event session that can break later typing
in a React input (guidepup/virtual-screen-reader#250). A test checks the
shipped script never calls setup(.

test/csp.test.js serves a page with `default-src 'none'; script-src 'none'`
over a local HTTP server and checks audit, scan announcements and the
record overlay; it fails on the old loader. Version 0.13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Elizabeth1979
Elizabeth1979 merged commit 936caea into main Oct 17, 2026
3 checks passed
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