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:
if (import.meta.env.DEV) {
const { mount } = await import("a11y-hud");
mount();
}Or use dead-code elimination so the import is stripped entirely:
// 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
<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:
mount({ scope: "#app" });
// or
<a11y-hud scope="#app"></a11y-hud>Internally, the selector is passed to document.querySelector(selector). An invalid CSS selector will throw a DOMException at scan time. Since scope is a developer-supplied option (not user input from the page), this is acceptable — the developer who configures the tool is responsible for passing a valid selector.
Mitigation status: The selector comes from developer configuration, not from page content. It is not sanitized before being passed to querySelector, but no sanitization is needed for a dev-tool API that only a developer controls.
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
stringruleId. - Each entry's
selector(if present) must be astring. - 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 CSS selectors generated by axe-core for the affected DOM nodes.
axe-core static content (violation IDs, descriptions, help text, help URLs) is trusted and not escaped — these are authored strings from a vendored dependency with a known and audited format.
Node selectors (the CSS selector paths shown under each violation) are generated by axe-core from the actual DOM. If the inspected page contains element IDs or class names with HTML special characters, those characters will appear unescaped in the panel's Shadow DOM.
Isolation: The panel lives entirely inside a Shadow Root (mode: "open"). Injected markup cannot escape to the host page via normal DOM traversal. The worst-case outcome is cosmetic corruption of the HUD panel itself, scoped to the developer's own screen.
Keyboard mode node selectors are built by a11y-hud's own buildSelector helper and ARE escaped via escapeHtml before rendering.
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.