Skip to content

Security model ​

a11y-hud is a developer-only tool. It is designed to run in development and staging environments. This page explains the security model, the specific mitigations in place, and the known limitations that are acceptable given the dev-only context.

Dev-only by design ​

a11y-hud should never be included in a production build. Bundle it behind a build-time environment check:

js
if (import.meta.env.DEV) {
  const { mount } = await import("a11y-hud");
  mount();
}

Or use dead-code elimination so the import is stripped entirely:

js
// Vite / Rollup
if (process.env.NODE_ENV !== "production") {
  import("a11y-hud").then(({ mount }) => mount());
}

Why this matters: a11y-hud bundles axe-core (~300 KB minified), injects a floating UI, attaches a MutationObserver, and writes to localStorage. None of these belong in a production build.

Trust model by path ​

npm + bundler ​

When you import from npm, a11y-hud code is bundled by your own build tool and served by your own dev server. The bundle is treated as trusted first-party code, the same as any other npm dependency.

CDN <script> tag ​

html
<script src="https://cdn.jsdelivr.net/npm/a11y-hud/dist/index.umd.js"></script>

This loads the UMD bundle from jsDelivr over HTTPS. You are trusting jsDelivr to serve the correct content and trusting that the published npm package has not been compromised. This is the same trust level as any CDN-loaded open-source library.

Bookmarklet ​

The bookmarklet loads the UMD bundle from jsDelivr at click time and calls window.A11yHud.mount(). The CSP implications are covered in the CSP compatibility guide.

The bookmarklet trust model is identical to running any developer browser extension or script that loads external code. It is appropriate for poking at staging environments, not for automated testing pipelines.

Scope injection ​

The scope option accepts a CSS selector string:

js
mount({ scope: "#app" });
// or
<a11y-hud scope="#app"></a11y-hud>

Internally, the selector is passed to document.querySelector(selector) inside a try/catch. A syntactically invalid selector, or one that matches nothing, silently falls back to scanning document.body — it never throws. Since scope is a developer-supplied option (not user input from the page), no further sanitization is needed.

Ignore-rules persistence (localStorage) ​

The ignore list is serialized to localStorage under the key a11y-hud:ignores. The importIgnores function parses JSON from this storage and from user-supplied files. Input validation is applied on import:

  • The parsed value must be an array.
  • Each entry must have a string ruleId.
  • Each entry's selector (if present) must be a string.
  • Entries that fail validation are silently dropped.

Malformed localStorage data (e.g., written by a different tool or corrupted) is silently ignored and does not throw.

Violation HTML rendering ​

axe-core results are rendered into the HUD panel's Shadow DOM. The content comes from axe-core's static rule definitions (violation IDs, descriptions, help URLs) and from strings derived from the inspected page's DOM.

axe-core static content (violation IDs, descriptions, help text, help URLs) is trusted and rendered as-is — these are authored strings from a bundled dependency with a known format.

Dynamic strings — anything derived from the inspected page — are HTML-escaped with escapeHtml before rendering. This covers the node selectors shown under each violation (generated by axe-core from the page's element IDs and class names), the selectors and messages in keyboard mode, and the error message shown in the "Scan failed" state.

Isolation: The panel lives entirely inside a Shadow Root (mode: "open"). Even if an unescaped string were rendered, the markup could not escape to the host page via normal DOM traversal — the worst-case outcome would be cosmetic corruption of the HUD panel itself, scoped to the developer's own screen.

Shadow DOM isolation ​

The HUD renders inside a Shadow Root. This means:

  • Host page CSS cannot style the panel (no unintended visual breakage).
  • Panel CSS cannot leak to the host page.
  • The panel's DOM is not part of the host page's normal DOM tree.

The Shadow Root is created with mode: "open". This is intentional: open mode lets developer tools (e.g., browser DevTools, other a11y tools) inspect the panel's internals. Closed mode would impede debugging and is inappropriate for a dev tool.

axe-core and cross-origin iframes ​

axe-core can inspect same-origin <iframe> contents. Cross-origin iframes are automatically skipped — this is a browser security constraint enforced by the browser, not by a11y-hud.

Vulnerability reporting ​

See SECURITY.md for the vulnerability reporting process.