lxml_html_clean CVE-2026-49825
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
Network-reachable stored XSS with low attacker complexity and no auth on anonymous input; victim must click (UI:R) and script runs cross-origin in the render context (S:C), giving C:H/I:L.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
Lifecycle Timeline
2Blast Radius
ecosystem impact- 1,090 pypi packages depend on lxml-html-clean (188 direct, 911 indirect)
Ecosystem-wide dependent count for version 0.4.5.
DescriptionGitHub Advisory
lxml_html_clean.Cleaner does not strip javascript: URLs from namespaced URL attributes (xlink:href)
Reporter: Guillem Lefait <guillem@datamq.com> · Date: 2026-05-10 Affected: lxml ≤ 6.1.0 and lxml_html_clean ≤ 0.4.4 (latest stable) Confirmed against: lxml 6.1.0 + lxml_html_clean 0.4.4 on Python 3.13.5, 3.14.4, and 3.15.0a8 (libxml2 2.14.6 / 2.9.14 - bug is in pure-Python sanitizer logic, independent of the libxml2 backend) Root-cause class: same as CVE-2021-28957 (formaction missing from link_attrs)
Summary
Cleaner filters URL schemes (javascript:, vbscript:, …) by walking links via rewrite_links(), which delegates to iterlinks(), which only yields attributes named in lxml.html.defs.link_attrs. That allow-list contains no prefixed names (xlink:href) and no srcset. As a result, when Cleaner is configured with safe_attrs_only=False - a documented option for callers that want lenient attribute handling but still expect URL-scheme scrubbing - <a xlink:href="javascript:…"> survives sanitization untouched, and any browser that follows the SVG-anchor specification will execute the JavaScript when the rendered link is clicked.
CWE: CWE-79 (XSS), with CWE-184 (Incomplete List of Disallowed Inputs) as the underlying defect class.
Affected components
| Package | Versions tested | File / line |
|---|---|---|
lxml | 4.9.x, 5.2.1, 6.1.0 | src/lxml/html/defs.py:20 |
lxml | " | src/lxml/html/__init__.py:485-528 |
lxml_html_clean | 0.4.0 - 0.4.4 | lxml_html_clean/clean.py:348,576 |
The legacy lxml.html.clean module - bundled in lxml < 5.2.0 and still installable on newer versions via the lxml[html_clean] extra - shares the same bug.
Root cause
defs.link_attrs is a flat string set; the literal xlink:href is absent:
# lxml/html/defs.py
link_attrs = frozenset([
'action', 'archive', 'background', 'cite', 'classid',
'codebase', 'data', 'href', 'longdesc', 'profile', 'src',
'usemap', 'dynsrc', 'lowsrc', 'formaction',
])HtmlMixin.iterlinks() (lxml/html/__init__.py:526-528) only yields attributes whose key is in that set:
for attrib in link_attrs:
if attrib in attribs:
yield (el, attrib, attribs[attrib], 0)Cleaner.__call__ registers the URL-scheme filter via rewrite_links (lxml_html_clean/clean.py:348), which is a thin wrapper around iterlinks(). Because xlink:href is never yielded, _remove_javascript_link (clean.py:576) is never invoked for it.
Minimal reproducer
from lxml import html
from lxml_html_clean import Cleaner
for payload in (
'<svg><a xlink:href="javascript:alert(1)">x</a></svg>',
'<math><a xlink:href="javascript:alert(2)">y</a></math>',
):
tree = html.fromstring(payload)
Cleaner(safe_attrs_only=False)(tree)
print(html.tostring(tree).decode())
print(' iterlinks:', list(html.fromstring(payload).iterlinks()))
# <svg><a xlink:href="javascript:alert(1)">x</a></svg> ← unchanged
# iterlinks: [] ← link rewriter blind
# <math><a xlink:href="javascript:alert(2)">y</a></math> ← unchanged
# iterlinks: [] ← link rewriter blindBoth SVG and MathML scopes are vulnerable - same allow-list gap, both render anchors that browsers treat as navigable. Other lab-confirmed surviving variants (same scope, different scheme encoding): mixed-case (JaVaScRiPt:), HTML-entity (javascript:), embedded tab (java\tscript:).
Impact
A caller that uses Cleaner to neutralise untrusted HTML and chooses safe_attrs_only=False - typically because the application wants to allow custom data-/aria-/vendor attributes - will silently pass javascript: payloads carried on xlink:href through to victim renders. Stored XSS in any application that round-trips user-supplied HTML through this configuration. Reach is conditional on the safe_attrs_only=False toggle, but that is a documented public option; consumers reasonably expect URL-scheme scrubbing to be independent of attribute allow-listing.
Suggested fix
Extend link_attrs to include xlink:href. In HTML mode, lxml.html keeps prefixed attribute names verbatim - the parsed key is the literal string xlink:href, not a Clark-notation form - so the existing allow-list lookup is a plain string match. Same shape as the CVE-2021-28957 fix:
# lxml/html/defs.py
link_attrs = frozenset([
'action', 'archive', 'background', 'cite', 'classid',
'codebase', 'data', 'href', 'longdesc', 'profile', 'src',
'usemap', 'dynsrc', 'lowsrc', 'formaction',
+ 'xlink:href',
])This single change closes the reported XSS for both SVG <a xlink:href> and MathML <a xlink:href>. lxml_html_clean is the canonical home of the Cleaner code (881 lines); lxml.html.clean is a 21-line backward-compat shim (from lxml_html_clean import *) that picks up the fix automatically once link_attrs is updated upstream. Since the upstream change requires lxml maintainer action, see the alternative below if a self-contained patch in lxml_html_clean is preferred.
Alternative (in-package fix, no lxml coordination needed): add a namespaced-URL-attribute walk inside Cleaner.__call__ so the URL-scheme filter doesn't depend on link_attrs. Sketch:
# lxml_html_clean/clean.py - supplements rewrite_links() in __call__
_NS_URL_ATTRS = ('xlink:href',)
# extend as needed
_BAD_SCHEME = re.compile(r'^\s*(javascript|vbscript|data):', re.I)
for el in doc.iter():
for attr in _NS_URL_ATTRS:
if attr in el.attrib and _BAD_SCHEME.match(el.attrib[attr]):
del el.attrib[attr]This decouples the cleaner from the upstream link_attrs set and matches the security-ownership boundary established when the cleaner was extracted in lxml 5.2.0.
Defense in depth (optional, regardless of which fix path is taken):
- Also handle
srcset: the value is aurl 1x, url 2x, …descriptor list, so split on commas and validate each candidate URL. Not directly executable in current browsers, but closes the same gap. - Also accept Clark-notation forms (
{http://www.w3.org/1999/xlink}href) so XML-mode callers usinglxml.etreeget the same protection. HTML mode never produces this form, so not needed for the reported bug.
Severity
CVSS 3.1 base score: 8.2 / High - AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N (stored XSS; victim must click the SVG anchor; scope-changed because script executes in the rendering origin). PR:N reflects the common case where untrusted HTML enters the sanitizer from anonymous sources (comments, support tickets); deployments that gate writes behind authentication can score with PR:L (→ 7.6).
Severity is CONDITIONAL on the caller passing safe_attrs_only=False. With the class default (True), attribute allow-listing strips xlink:href before scheme scrubbing runs, and the bug does not fire - verified at HEAD: default-config Cleaner()(<svg><a xlink:href="javascript:…">x</a></svg>) → <svg><a>x</a></svg>.
Prior art / novelty
- CVE-2021-28957 (lxml 4.6.3) - same root cause, different attribute (
formaction). Fix was a one-line extension oflink_attrs. Direct precedent. - CVE-2022-34473 (Mozilla Sanitizer API) -
xlink:hrefURL bypass primitive in a different sanitizer. - Bleach (Mozilla, Python) explicitly handles the
xlinknamespace;enshrined/svg-sanitize(PHP) shipscleanXlinkHrefs(); DOMPurify scrubsxlink:hrefviaALLOWED_URI_REGEXP. nh3(the alternative recommended inlxml_html_clean's own README for security-sensitive use) is not vulnerable to this primitive - verified 2026-05-10 onnh3==0.3.5: with<svg>/<math>/<a>andxlink:hrefexplicitly added totags/attributes, both SVG and MathML payloads, all four scheme-encoding variants, are stripped (output e.g.<svg><a rel="noopener noreferrer">x</a></svg>).
Coordination
Filing as a private GHSA at fedora-python/lxml_html_clean - lxml_html_clean is the canonical maintainer of the Cleaner code (881 lines) and the security-responsible team since the lxml 5.2.0 split, where the cleaner was extracted out of lxml precisely so cleaner-security reports could land on the right team. The lxml side cannot be filed via GHSA (https://github.com/lxml/lxml/security/advisories/new returns 404 - private reporting is not enabled), so a parallel report has been emailed directly to the lxml maintainer for the upstream defs.link_attrs patch path. You're welcome to coordinate with them directly if you'd prefer the upstream fix over the in-package alternative above.
Happy to provide a draft patch or PR on either path. No bounty expected.
Articles & Coverage 1
AnalysisAI
Stored cross-site scripting in the lxml_html_clean Cleaner (≤ 0.4.4) and the bundled lxml legacy html.clean module (≤ 6.1.0) lets attackers smuggle javascript: URLs through HTML sanitization on the namespaced xlink:href attribute. When callers configure Cleaner with safe_attrs_only=False, the URL-scheme scrubber never inspects <a xlink:href="javascript:..."> inside SVG or MathML, so the payload survives and executes when a victim clicks the rendered anchor. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Requires the target application to (1) sanitize untrusted HTML with lxml_html_clean's Cleaner (or lxml.html.clean.Cleaner) explicitly configured with safe_attrs_only=False - the decisive prerequisite, since the class default safe_attrs_only=True strips xlink:href and the bug does not fire; (2) store and re-render the sanitized output to other users; and (3) a victim must actively click the malicious SVG or MathML <a xlink:href> anchor (UI:R) in a browser that follows the SVG-anchor navigation spec. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The provided CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N = 8.2 High) rates this as network-reachable, low-complexity, unauthenticated stored XSS requiring victim interaction with scope change - consistent with a sanitizer bypass. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker submits comment or support-ticket HTML containing <svg><a xlink:href="javascript:alert(document.cookie)">click</a></svg> to an application that sanitizes user HTML with Cleaner(safe_attrs_only=False) and later renders it to other users. The sanitizer passes the payload through untouched (a working POC demonstrates iterlinks() is blind to xlink:href); when a victim viewing the page clicks the SVG anchor, the script executes in the site's origin, enabling session/cookie theft. … |
| Remediation | No vendor-released patched version was identified at time of analysis (the report is a private/coordinating GHSA with a proposed fix, not a tagged release), so monitor advisory GHSA-4jhm-jv67-739f (https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-4jhm-jv67-739f) and upgrade to the fixed lxml_html_clean release once published; the upstream fix adds 'xlink:href' to lxml.html.defs.link_attrs. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
24 hours: Inventory all applications and services using lxml ≤6.1.0 or lxml_html_clean ≤0.4.4; review configurations for safe_attrs_only=False. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-4jhm-jv67-739f