Skip to main content

lxml_html_clean CVE-2026-49825

HIGH
Cross-site Scripting (XSS) (CWE-79)
2026-07-08 https://github.com/fedora-python/lxml_html_clean GHSA-4jhm-jv67-739f
8.2
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory 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

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
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Confidentiality
High
Integrity
Low
Availability
None

Lifecycle Timeline

2
Analysis Generated
Jul 08, 2026 - 21:21 vuln.today
CVE Published
Jul 08, 2026 - 20:23 github-advisory
HIGH 8.2

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 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

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.

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

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

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.

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

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-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-49825 vulnerability details – vuln.today

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