Skip to main content

lxml_html_clean CVE-2026-49825

| EUVDEUVD-2026-63390 HIGH
Cross-site Scripting (XSS) (CWE-79)
2026-07-08 https://github.com/fedora-python/lxml_html_clean GHSA-4jhm-jv67-739f PYSEC-2026-2614
8.2
CVSS 3.1 · Vendor: https://github.com/fedora-python/lxml_html_clean
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Access
    Submit HTML with xlink:href javascript payload via user input
  2. Delivery
    Cleaner(safe_attrs_only=False) fails to scrub namespaced attribute
  3. Exploit
    Malicious SVG/MathML anchor stored and served to users
  4. Execution
    Victim clicks rendered anchor
  5. Impact
    JavaScript executes in site origin, session hijack

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A05:2025 - Injection
ASVS 5.0
V1 - Encoding and Sanitization Output encoding, context-aware escaping
MITRE ATT&CK

2 techniques across 2 tactics: Initial Access, Execution

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

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. The payload must ride specifically on the namespaced xlink:href attribute (plain href is already filtered). Limiting factors: non-default configuration required, victim click required, and only javascript:/vbscript:/data:-style scheme payloads on xlink:href are affected. PR:N (unauthenticated) holds only for anonymous input surfaces; deployments that require authentication to submit HTML effectively raise the bar to PR:L.

Impact

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. The most important limiting signal is not in the raw vector: exploitation is CONDITIONAL on the caller passing safe_attrs_only=False, a documented but non-default option; under the class default (safe_attrs_only=True) attribute allow-listing removes xlink:href before scheme scrubbing runs and the bug does not fire (verified in the advisory). This meaningfully narrows real-world exposure to the subset of deployments that opt out of the default. No EPSS score and no KEV listing were provided, so widespread automated exploitation is not evidenced; however a reliable POC exists, lowering the bar for opportunistic use. PR:N is defensible for anonymous input surfaces (comments, tickets); deployments that gate HTML submission behind authentication should re-score at PR:L (~7.6). Net: a genuine, easily-triggered XSS for the affected configuration, but not an internet-wide emergency - prioritize by whether your code sets safe_attrs_only=False.

Exploit ScenarioDerived

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. Because the attacker-controlled content is stored and re-served, this is persistent stored XSS.

This Week Schedule remediation this week. Why: HIGH severity (CVSS 7.0-8.9).
Patch available 0.2% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jul 08, 2026

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. Publicly available exploit code exists (a working reproducer ships with the advisory), but there is no public exploit identified as being used in active attacks and the issue is not in CISA KEV.

RemediationAI

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. As an immediate compensating control that does not depend on a release, revert to the safe default Cleaner(safe_attrs_only=True), which strips xlink:href before rendering - the trade-off is losing the custom data-/aria-/vendor attributes you enabled by setting safe_attrs_only=False. If you must keep safe_attrs_only=False, add a post-sanitization pass that deletes any namespaced URL attribute (xlink:href, and also validate each srcset candidate) whose value matches ^\s*(javascript|vbscript|data): case-insensitively and after entity/tab decoding, or switch security-sensitive sanitization to nh3, which the lxml_html_clean README recommends and which the advisory verified is not vulnerable to this primitive. Also normalize scheme-obfuscation variants (mixed-case JaVaScRiPt:, HTML-entity java&#x73;cript:, embedded-tab java\tscript:) since those bypass naive string checks.

Affected VersionsENISA

lxml < 6.1.1
lxml_html_clean < 0.4.5

Affected Packages

pypi lxml-html-clean fix: 0.4.5
euvd lxml fix: 6.1.1

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 2 packages in pypi, euvd

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

The affected component is the pure-Python HTML sanitizer maintained in the lxml_html_clean package (the code extracted from lxml core in 5.2.0; lxml.html.clean is now a thin backward-compat shim). Cleaner strips dangerous URL schemes by walking links through rewrite_links() -> iterlinks(), which only yields attributes listed in lxml.html.defs.link_attrs. That allow-list (action, href, src, formaction, etc.) contains no namespaced/prefixed names, so xlink:href (and srcset) are never handed to the _remove_javascript_link filter. This is a textbook CWE-184 (Incomplete List of Disallowed Inputs) defect manifesting as CWE-79 (XSS): the sanitizer's disallow logic has a gap rather than a broken filter. Because libxml2 parses prefixed HTML attribute names verbatim (the literal string 'xlink:href', not Clark notation), the fix is a plain string-set extension. The root cause is identical to CVE-2021-28957, where the same link_attrs set was missing formaction.

Know when Python gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/fedora-python/lxml_html_clean) PRIMARY 8.2 HIGH
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
vuln.today AI 8.2 HIGH

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.

3.1 AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
SUSE HIGH
Red Hat 8.2 HIGH

Primary rating from Vendor (https://github.com/fedora-python/lxml_html_clean).

CVSS VectorVendor: https://github.com/fedora-python/lxml_html_clean

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Confidentiality
High
Integrity
Low
Availability
None

DescriptionCVE.org

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

PackageVersions testedFile / line
lxml4.9.x, 5.2.1, 6.1.0src/lxml/html/defs.py:20
lxml"src/lxml/html/__init__.py:485-528
lxml_html_clean0.4.0 - 0.4.4lxml_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:

python
# 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:

python
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

python
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 blind

Both 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 (java&#x73;cript:), 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:

diff
# 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:

python
# 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 a url 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 using lxml.etree get 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 of link_attrs. Direct precedent.
  • CVE-2022-34473 (Mozilla Sanitizer API) - xlink:href URL bypass primitive in a different sanitizer.
  • Bleach (Mozilla, Python) explicitly handles the xlink namespace; enshrined/svg-sanitize (PHP) ships cleanXlinkHrefs(); DOMPurify scrubs xlink:href via ALLOWED_URI_REGEXP.
  • nh3 (the alternative recommended in lxml_html_clean's own README for security-sensitive use) is not vulnerable to this primitive - verified 2026-05-10 on nh3==0.3.5: with <svg>/<math>/<a> and xlink:href explicitly added to tags/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.

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 4 pypi packages depend on lxml-html-clean (3 direct, 1 indirect)

Ecosystem-wide dependent count for version 0.4.5.

Executive ViewAI

Why It Matters

lxml_html_clean (≤0.4.4) and lxml legacy html.clean (≤6.1.0) contain a stored cross-site scripting vulnerability that bypasses HTML sanitization when safe_attrs_only=False, enabling attackers to inject malicious JavaScript through SVG/MathML namespaced attributes. With publicly available exploit code in circulation, applications relying on these libraries face immediate risk of account compromise, data theft, and malware distribution when processing user-supplied HTML content. Organizations must immediately assess their dependency footprint and implement compensating controls pending patch availability.

Business Impact: High
Compensating Controls

No vendor-released patch identified at time of analysis. Interim mitigations: (1) Disable safe_attrs_only=False if application requirements permit; (2) Implement supplementary URL scheme filtering for SVG/MathML xlink:href attributes; (3) Apply Web Application Firewall rules to block javascript: scheme injection attempts; (4) Require manual content review for stored HTML in security-sensitive contexts.

Threat Intelligence Overview

EPSS Score
0.2%
Top 85% exploitable
Prevalence
High
lxml is ubiquitous in Python; its Cleaner sanitizer is widely deployed, though the vulnerable config is non-default
Threat Intel
1
1 sources
Data Quality
7 sources
Confidence: High

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity HIGH
CISA KEV No
Public POC No
EPSS 0.2%
Patch available Yes
Active threat No
Deciding rule
HIGH severity (CVSS 7.0-8.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
None
System Exposure· derived
Open
Automatable· derived
No
Human Impact
High

Priority Score

41
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +41
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Cross-Site Scripting occurs when an application accepts untrusted data and sends it to a web browser without proper validation or encoding.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

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

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-49869 CRITICAL POC
10.0 Jun 26

Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

Vendor StatusVendor

SUSE

Severity: Important
Product Status
openSUSE Tumbleweed Fixed

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy