Security audit

This page publishes security and code-health audit results for a11yequitas.org. Audits run at each release.

Last run: 2026-07-19 (code health + dependency scan; other categories last ran 2026-05-25). Next run: on next release.

Methodology

  • vitest — component and route security assertions: no dangerously set HTML outside approved inline scripts, no external script sources beyond the analytics stub, no cross-origin data leakage.
  • Fallow — codebase intelligence tool (Rust). Measures dead code, duplication, cyclomatic complexity, unit size, and coupling. Produces a 0–100 health score with a letter grade. Integrated into the release checklist.
  • pnpm audit — dependency vulnerability scan run on every release via pnpm run audit (chained with Vitest and Fallow). High and critical advisories block release.
  • HTTP security headers — verified via automated check and manual inspection. Target headers: Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy.

Grades by category

Security audit grades. A = no findings. F = critical unresolved.
CategoryDateGradeFindings
Code health (Fallow)2026-07-19B (79)Score 79 / 100 from fallow health --score (fallow-cli 2.85.0). Dead-code analysis is fully clean: 0 issues (1 documented suppression, 0 stale), 110 entry points, dead files 0.0 %, dead exports 0.0 %, no circular deps. Metrics: 24,940 LOC analyzed, avg maintainability 95.3 (good), avg cyclomatic 1.9 / p90 4, 42 / 638 functions above complexity threshold. Reported duplication is 49.1 % across 65 files — the large majority is the intentional English/Spanish page-pair mirroring (each hand-translated page.es.tsx is a deliberate structural clone of its English sibling), now included in analysis since the Spanish siblings were added as entry points; this is bilingual architecture, not copy-paste debt. Previously-open findings all resolved 2026-07-19: public/static/styles.css is referenced by five live legacy HTML pages (excluded from JS-graph analysis with a documented reason); AIDoc/SRReport type exports un-exported (in-file use only); /pagefind/pagefind.js added to ignoreUnresolvedImports (generated at build time, expected). Numeric score refreshes when the audit runs on the production host.
Dependency scan (pnpm audit)2026-07-19A0 critical, 0 high, 0 moderate, 0 low. Six transitive devDependency advisories (2 high: tmp path traversal GHSA-ph9p-34f9-6g65, vite fs.deny bypass GHSA-fx2h-pf6j-xcff; 3 moderate: uuid GHSA-w5hq-g745-h8pq, launch-editor GHSA-v6wh-96g9-6wx3, js-yaml merge-key DoS; 1 low: tmp symlink GHSA-52f5-9888-hmc6) resolved 2026-07-19 via scoped pnpm overrides — parent API usage was verified compatible before forcing each bump. Prior postcss advisory (GHSA-qx2v-qp2m-jg93) remains resolved by the earlier override.
HTTP security headers2026-05-25B+HTTPS is enforced: plain HTTP returns 301 to https://a11yequitas.org/. Live response includes Content-Security-Policy, Strict-Transport-Security: max-age=31536000; includeSubDomains; preload, X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, and Permissions-Policy: camera=(), microphone=(), geolocation=(). Follow-up: raise HSTS to max-age=63072000, add interest-cohort=(), and keep frame-ancestors 'none' as the effective anti-framing control.
CSP policy2026-05-25BActive policy observed on live HTTPS response: default-src 'none'; scripts limited to same-origin inline code plus Umami and Cloudflare Insights; same-origin styles, images, fonts, workers, and Pagefind are allowed; base-uri 'self', form-action 'none', and frame-ancestors 'none' are set. Follow-up: remove 'unsafe-inline' and 'wasm-unsafe-eval' from script-src after migrating inline scripts to nonces or hashes, and re-check any map tile image requirements before tightening img-src.
XSS / injection surface2026-05-25AStatic export — no server-side rendering, no live form submissions, no eval, no Function(), no document.write, no direct .innerHTML = in production source. Test-only matches limited to src/__tests__/static-html.aaa.test.ts (document.write(html) + w.eval(axeSource)) used to load generated static HTML and inject axe into the test DOM. 12 dangerouslySetInnerHTML uses: 11 inline JSON-LD / theme-restore string literals; 1 (SearchDialog Pagefind excerpts) injects build-time HTML from same-origin index — trust boundary noted in source.
Third-party scripts2026-05-25AOne self-hosted Umami analytics script (defer, no cookies). No other third-party JS. No CDN-loaded fonts or icons. All target="_blank" links carry rel="noopener noreferrer".
Privacy / data collection2026-05-25ANo cookies. No PII collected. Font and theme preferences stored in localStorage only (strings, no identifiers). No network fetch in production (the one dev-only POST in ScreenReaderChecklist is gated by NODE_ENV !== "production").
Accessibility regression (Vitest + axe)2026-05-25A70 / 70 pages pass axe-core's automated WCAG 2.2 AAA rule checks in happy-dom (a subset of full AAA — manual/AT review still required). Locks in Playwright findings at the file level. Run via pnpm run audit:vitest (23.17 s).
Accessibility audit (Playwright + axe + Alfa)2026-05-25A352 / 352 tests pass. 32 routes × axe (6 theme×font combos) + Alfa per route + structural assertions (lang, h1, title, skip-link).

Static site posture

a11yequitas.org is a fully static Next.js export. There is no server-side execution, no database, no authentication surface, and no form submission handler. The attack surface is limited to:

  • Third-party analytics script injection risk (one script, defer-loaded)
  • CDN / hosting layer (Podman + Nginx on Rocky Linux)
  • localStorage — stores font and theme preference strings only; no sensitive data

Known findings

  • postcss < 8.5.10 (resolved 2026-05-24) — XSS in CSS stringify output (GHSA-qx2v-qp2m-jg93). Fixed by pnpm override in pnpm-workspace.yaml; all consumers now resolve topostcss 8.5.15.
  • Fallow code health: unit_size — page components still inline 200–400 LOC of JSX content per file. Wrapper boilerplate already extracted to ProseArticle; remaining work is to split each page's inline sections (alerts, tables, bullet lists) into co-located sub-components. 42 of 638 analyzed functions remain above complexity threshold (the analyzed universe grew on 2026-07-19 when Spanish page siblings and CLI scripts were added as entry points). Tracked.
  • Fallow code health: duplication 49.1 % — the headline number rose on 2026-07-19 because the hand-translated Spanish page siblings (page.es.tsx) were added to analysis as entry points; each is a deliberate structural clone of its English page, so most reported duplication is intentional bilingual architecture, not copy-paste debt. The genuine content-clone remainder (WCAG bullet lists, accessible table markup, "Keyboard interaction" sections shared across tutorials) still needs shared content components (e.g., <WcagBulletList>) and is scoped for a content refactor cycle.
  • Fallow dead-export / unused findings — resolved 2026-07-19public/static/styles.css is referenced by five live legacy HTML pages (excluded from JS-graph analysis with a documented reason); AIDoc and SRReport type exports were un-exported (in-file use only); the manually-run CLI scripts under scripts/ and the Spanish page.es.tsx siblings were added as declared entry points. Dead-code analysis now reports 0 issues.
  • Fallow unresolved import (expected, config-suppressed) /pagefind/pagefind.js is generated at build time into out/pagefind/ and loaded dynamically from the same origin. Added to ignoreUnresolvedImports in .fallowrc.json on 2026-07-19 with this rationale. Not a security finding.
  • SearchDialog Pagefind excerpt injection (low)dangerouslySetInnerHTMLrenders the Pagefind index excerpt HTML. Source is the same-origin index built from this site's own content; no third-party content is indexed. Trust boundary documented inline. If the index ever ingests external content, sanitize before render.

Report a security issue

To report a security issue, contact A11y Equitas via a11yequitas.org. Please do not file public issues for unresolved security vulnerabilities.