Skip to content

State of the Project and Roadmap

Cure53 edited this page Jun 12, 2026 · 4 revisions

State of the Project and Roadmap

A birds-eye assessment of where DOMPurify stands across every dimension that matters for a security-critical, very widely deployed sanitizer, followed by a near/mid/long-term roadmap. Written at v3.4.9, just after the legacy-browser testing and the local code-coverage tooling landed; updated after the post-3.4.9 code-quality refactor run (#1473-#1475) and the CI matrix fix.

Part 1 - Aspect-by-aspect assessment

# Aspect Status One-line read
1 Core sanitization and security research Excel The differentiator: deep mXSS/namespace/clobbering defenses, active frontier hardening
2 Supply-chain and release integrity Excel Sigstore + SLSA + scorecard + CodeQL + osv + pinned actions + signed tags
3 Zero runtime dependencies Excel No third-party runtime code in the trust boundary
4 Environment and browser test breadth Excel jsdom (Node 20-26), Playwright 3 engines x 3 OS, and now older-engine snapshots
5 Packaging, modules, and types Excel CJS + ESM + UMD, dual typings, exports map, cross-module consumption harness
6 Governance and disclosure Excel SECURITY.md, bug bounty, PGP, CoC, CODEOWNERS, templates, dependabot
7 CI/CD pipeline Excel Skip-mirroring and required-checks correct; browser-suite-once-per-PR redundancy closed
8 Documentation Solid Strong README + wiki; some updates drafted-not-published; no generated API reference
9 API/config design and Trusted Types Solid Mature surface incl. RETURN_TRUSTED_TYPE; large config = power and foot-guns
10 Code-coverage visibility Done Local npm run coverage (istanbul); per-config-path line/branch report, ~91% / 86.5% stmt/branch in jsdom
11 Performance and DoS resistance Part Benchmark harness landed (npm run bench, jsdom, directional); pathological-input guard tests still open
12 Native Sanitizer API strategy Solid DOMPurify inspired the spec and a maintainer co-authors it; README positions it. Open: a user-facing fallback snippet
13 Edge-case hardening backlog Tracked A few browser-only items identified but not yet fixed or formally classed
14 Metadata freshness Done npm description rewritten to drop the stale IE 10+/Opera enumeration; lands on next publish
15 Continuity / bus factor Watch Maintainer-centric; manual publish is deliberate but a single point

Where you already excel (keep doing this)

Core security and research. This is the moat. The allowlist model, namespace integrity, the mXSS/clobbering/template defenses, and the ongoing IN_PLACE and parser-mutation hardening are work almost no other library does at this depth. The attack-classes catalog and threat-model docs make the reasoning legible.

Supply-chain security. This is genuinely best-in-class for an OSS library: Sigstore-signed artifacts, SLSA provenance, GPG-signed annotated tags, OSSF Scorecard, CodeQL, dependency-review, osv-scanner, SHA-pinned actions, and a hardened, deliberately-manual npm publish with a written rationale. Most projects of this reach do a fraction of this.

Zero runtime dependencies. For a security boundary, the empty runtime dependency tree is a feature in itself: nothing to audit, nothing to get compromised upstream.

Test breadth. jsdom across five Node versions, real-browser coverage on three engines and three OSes, a fuzzer, and now a Playwright-only older-engine matrix. The new legacy coverage is strategically well-timed (see roadmap).

Packaging and types. Dual CJS/ESM with matched type declarations, a proper exports map, source maps, and a dedicated harness that verifies consumption across CommonJS, ESM, and nodenext resolution. This prevents a whole class of "works in my bundler" breakage.

Gaps and what closing them looks like

Code-coverage visibility (#10) - closed. There is now a local npm run coverage script: an istanbul-instrumented build runs the jsdom suite and writes a self-hosted HTML line/branch report (no third-party service, and deliberately not wired into CI, so it stays a developer tool rather than a gate). It is documented in the README. Driven the intended way - toward untested config permutations rather than a headline number - a focused batch of roughly 30 tests lifted jsdom coverage from 86.4% / 81.6% to about 91% / 86.5% statements/branches. Those tests closed the reachable gaps in utils.ts (the stringifyValue input-coercion path) and purify.ts (the user-supplied Trusted Types policy config and its re-entrancy guard, the forceKeepAttr/keepAttr hook decisions, the noscript/noembed fallback-mXSS guard, isValidAttribute, and several config branches), several of which double as genuine regression tests. What the jsdom report cannot reach is now well understood and not a true blind spot: the browser-only paths (global Trusted Types getAttributeType, <form> clobbering, shadow DOM), which the Playwright run exercises functionally, and a small set of dead/defensive arms (no-Reflect polyfills, guarded-unreachable switch cases). The one optional next step is merging the Playwright run's coverage so those browser-only paths also appear in the number.

Performance and DoS resistance (#11) - half closed. The benchmark half landed: npm run bench runs seven deterministic fixture scenarios against the built bundle with --json/--compare for A/B runs across branches. It is jsdom-based and explicitly directional (jsdom overweights innerHTML serialization relative to browsers); it has already earned its keep by validating one optimization with reproduced numbers and by honestly flagging another as within-noise. Still open: the DoS half - pathological-input guard tests with hard time bounds (deeply nested structures, reparse blow-ups), and a decision on whether opt-in depth/size limits belong in the API.

Native Sanitizer API strategy (#12). This is a strength, not a gap. DOMPurify inspired the HTML Sanitizer API, a maintainer co-authors the spec, and the README already references the relationship. The native API is shipping in several browsers but is not Baseline yet (Safari pending), so DOMPurify remains the required fallback today, the server-side sanitizer where there is no native API, and the advanced, extensible option (hooks, profiles, custom-element handling) that the native surface does not expose. The native API is not a threat - it is what makes those enduring roles clearer. What is still open is user-facing rather than strategic: a canonical feature-detect-and-fallback snippet in the docs, optional differential testing against the native API as a correctness oracle, and a decision on whether a "use native where present" thin mode is worth offering. None of that blocks anything; the positioning itself is settled and authoritative.

Edge-case hardening backlog (#13). A handful of browser-only items are identified but not yet resolved or formally classified (e.g. customizable-select re-mirroring, a reaction-driven attribute re-arm, a WebKit reparse DoS). Each needs a decision: fix, or document as an explicit non-goal with rationale. Leaving them in limbo is the only real risk.

Metadata freshness (#14) - resolved. The npm description advertised IE 10+ and a dated Opera/Blink/WebKit enumeration, while 3.x dropped MSIE and the build targets no longer include IE at all. It has been rewritten to drop the stale browser list and note Node.js (via jsdom) support, in DOMPurify's own voice. The change is to package.json, so it reaches npm on the next publish.

Continuity (#15). The manual-publish-from-laptop model is well-reasoned for its threat model, but it concentrates release capability in one person. A written continuity plan (backup signer/publisher, documented key recovery) is cheap insurance and does not require automating publish.

Part 2 - Roadmap

Near-term (this release cycle: 3.4.x / 3.5)

  • Done: the CI efficiency change - the three browser engines now run on one Node version in the install matrix (the same one browser-matrix uses), jsdom on all of them. 12 redundant browser suite runs dropped per PR, per-engine coverage unchanged, check names stable.
  • Done: a code-quality refactor pass (#1473-#1475): public types extracted to src/types.ts with a byte-identical runtime rebuild, duplicated defaults and dead branches removed, the three big sanitizer functions decomposed (src now lints with zero warnings), the benchmark harness added, and one measured perf win (probe regex hoisting plus textContent-before-innerHTML ordering, 3-9% median on jsdom, reproduced across interleaved runs).
  • Done: corrected the npm description (dropped the stale IE 10+/Opera claim, noted Node.js via jsdom); lands on the next publish.
  • Publish the drafted wiki updates (Security Goals and Threat Model, Attack Classes and Bypass History) so the public threat model matches the code.
  • Done: local code-coverage reporting via npm run coverage (istanbul-instrumented jsdom run, self-hosted HTML, deliberately not in CI). Optional follow-up: merge the Playwright run's coverage so the browser-only paths (Trusted Types, clobbering, shadow DOM) count too.
  • Triage the edge-case backlog: for each item, ship a fix or record an explicit non-goal with reasoning in the threat-model doc.

Mid-term (3.5 / 3.6)

  • Finish the DoS half of the performance story: pathological inputs with hard time bounds on top of the existing npm run bench harness; evaluate opt-in depth/size limits.
  • Differential fuzzing against the native Sanitizer API where it ships. This is both a correctness oracle and a research goldmine: every divergence between DOMPurify and setHTML() is either a DOMPurify finding or a spec finding.
  • Formalize the server-side support matrix: jsdom as supported, an explicit stance on happy-dom, and documented guarantees (and non-guarantees) for Node-side sanitization.
  • Config-safety guidance in the product, not just the wiki: optional warnings for known-dangerous option combinations, building on the dangerous-tags work.
  • Publish the native-API interop story: the canonical fallback pattern and, if warranted, a "native when available" mode.

Long-term (the next era)

  • Position DOMPurify for the post-Baseline world. When Safari ships and the native API approaches Baseline, DOMPurify's enduring roles are: (a) the polyfill/fallback for the long tail of older browsers - which the new legacy testing now lets you prove you support; (b) the server-side sanitizer, where there is no native API; (c) the advanced, extensible option (hooks, profiles, custom-element handling, richer config the native API does not expose); and (d) a conformance oracle for the native API, given the team's standards role.
  • Consider a thin "DOMPurify over native" build for environments that have the native API, trading bundle size for delegation while keeping the DOMPurify configuration ergonomics consumers already depend on.
  • Invest in sustainability: a small maintainer group and a continuity plan, so the project's security guarantees outlive any single person, funded through the existing sponsorship and bounty channels.
  • Keep the research edge sharp. New surfaces keep arriving (customizable-select, declarative shadow DOM, evolving foreign-content rules); being first to understand and defend them is what keeps DOMPurify the reference sanitizer.

One-paragraph synthesis

DOMPurify is in an unusually strong state: the security core, supply chain, and distribution are all at or near best-in-class, and the recent work closed the remaining test-coverage and CI gaps. The open items are now mostly forward-looking rather than remedial - the DoS guard tests and formalizing the edge-case backlog. The project's role alongside the arriving but not-yet-universal native Sanitizer API is already well defined, through a maintainer's co-authorship of the spec itself. The legacy-browser work was well-timed: the clearest long-term value of DOMPurify is precisely the places the platform will not reach for years (old browsers and the server), and you can now demonstrate coverage there.