Search

Find a vulnerability

Search criteria

    Related vulnerabilities

    GHSA-4JHM-JV67-739F

    Vulnerability from github – Published: 2026-07-08 20:23 – Updated: 2026-07-08 20:23
    VLAI
    Summary
    `lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes
    Details

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

     # 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 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 / HighAV: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_cleanlxml_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.

    Show details on source website

    {
      "affected": [
        {
          "package": {
            "ecosystem": "PyPI",
            "name": "lxml_html_clean"
          },
          "ranges": [
            {
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "fixed": "0.4.5"
                }
              ],
              "type": "ECOSYSTEM"
            }
          ]
        }
      ],
      "aliases": [
        "CVE-2026-49825"
      ],
      "database_specific": {
        "cwe_ids": [
          "CWE-184",
          "CWE-79"
        ],
        "github_reviewed": true,
        "github_reviewed_at": "2026-07-08T20:23:22Z",
        "nvd_published_at": null,
        "severity": "HIGH"
      },
      "details": "# `lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes (`xlink:href`)\n\n**Reporter:** Guillem Lefait \u003cguillem@datamq.com\u003e \u00b7 **Date:** 2026-05-10\n**Affected:** `lxml` \u2264 6.1.0 and `lxml_html_clean` \u2264 0.4.4 (latest stable)\n**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 \u2014 bug is in pure-Python sanitizer logic, independent of the libxml2 backend)\n**Root-cause class:** same as CVE-2021-28957 (`formaction` missing from `link_attrs`)\n\n## Summary\n\n`Cleaner` filters URL schemes (`javascript:`, `vbscript:`, \u2026) 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` \u2014 a documented option for callers that want lenient attribute handling but still expect URL-scheme scrubbing \u2014 `\u003ca xlink:href=\"javascript:\u2026\"\u003e` survives sanitization untouched, and any browser that follows the SVG-anchor specification will execute the JavaScript when the rendered link is clicked.\n\n**CWE:** CWE-79 (XSS), with CWE-184 (Incomplete List of Disallowed Inputs) as the underlying defect class.\n\n## Affected components\n\n| Package            | Versions tested        | File / line                      |\n|--------------------|------------------------|----------------------------------|\n| `lxml`             | 4.9.x, 5.2.1, 6.1.0    | `src/lxml/html/defs.py:20`       |\n| `lxml`             | \"                      | `src/lxml/html/__init__.py:485-528` |\n| `lxml_html_clean`  | 0.4.0 \u2013 0.4.4          | `lxml_html_clean/clean.py:348,576` |\n\nThe legacy `lxml.html.clean` module \u2014 bundled in `lxml \u003c 5.2.0` and still installable on newer versions via the `lxml[html_clean]` extra \u2014 shares the same bug.\n\n## Root cause\n\n`defs.link_attrs` is a flat string set; the literal `xlink:href` is absent:\n\n```python\n# lxml/html/defs.py\nlink_attrs = frozenset([\n    \u0027action\u0027, \u0027archive\u0027, \u0027background\u0027, \u0027cite\u0027, \u0027classid\u0027,\n    \u0027codebase\u0027, \u0027data\u0027, \u0027href\u0027, \u0027longdesc\u0027, \u0027profile\u0027, \u0027src\u0027,\n    \u0027usemap\u0027, \u0027dynsrc\u0027, \u0027lowsrc\u0027, \u0027formaction\u0027,\n])\n```\n\n`HtmlMixin.iterlinks()` (`lxml/html/__init__.py:526-528`) only yields attributes whose key is in that set:\n\n```python\nfor attrib in link_attrs:\n    if attrib in attribs:\n        yield (el, attrib, attribs[attrib], 0)\n```\n\n`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.\n\n## Minimal reproducer\n\n```python\nfrom lxml import html\nfrom lxml_html_clean import Cleaner\n\nfor payload in (\n    \u0027\u003csvg\u003e\u003ca xlink:href=\"javascript:alert(1)\"\u003ex\u003c/a\u003e\u003c/svg\u003e\u0027,\n    \u0027\u003cmath\u003e\u003ca xlink:href=\"javascript:alert(2)\"\u003ey\u003c/a\u003e\u003c/math\u003e\u0027,\n):\n    tree = html.fromstring(payload)\n    Cleaner(safe_attrs_only=False)(tree)\n    print(html.tostring(tree).decode())\n    print(\u0027  iterlinks:\u0027, list(html.fromstring(payload).iterlinks()))\n# \u003csvg\u003e\u003ca xlink:href=\"javascript:alert(1)\"\u003ex\u003c/a\u003e\u003c/svg\u003e     \u2190 unchanged\n#   iterlinks: []                                          \u2190 link rewriter blind\n# \u003cmath\u003e\u003ca xlink:href=\"javascript:alert(2)\"\u003ey\u003c/a\u003e\u003c/math\u003e   \u2190 unchanged\n#   iterlinks: []                                          \u2190 link rewriter blind\n```\n\nBoth SVG and MathML scopes are vulnerable \u2014 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\u0026#x73;cript:`), embedded tab (`java\\tscript:`).\n\n## Impact\n\nA caller that uses `Cleaner` to neutralise untrusted HTML and chooses `safe_attrs_only=False` \u2014 typically because the application wants to allow custom data-/aria-/vendor attributes \u2014 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.\n\n## Suggested fix\n\n**Extend `link_attrs`** to include `xlink:href`. In HTML mode, `lxml.html` keeps prefixed attribute names verbatim \u2014 the parsed key is the literal string `xlink:href`, not a Clark-notation form \u2014 so the existing allow-list lookup is a plain string match. Same shape as the CVE-2021-28957 fix:\n\n```diff\n # lxml/html/defs.py\n link_attrs = frozenset([\n     \u0027action\u0027, \u0027archive\u0027, \u0027background\u0027, \u0027cite\u0027, \u0027classid\u0027,\n     \u0027codebase\u0027, \u0027data\u0027, \u0027href\u0027, \u0027longdesc\u0027, \u0027profile\u0027, \u0027src\u0027,\n     \u0027usemap\u0027, \u0027dynsrc\u0027, \u0027lowsrc\u0027, \u0027formaction\u0027,\n+    \u0027xlink:href\u0027,\n ])\n```\n\nThis single change closes the reported XSS for both SVG `\u003ca xlink:href\u003e` and MathML `\u003ca xlink:href\u003e`. `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.\n\n**Alternative (in-package fix, no lxml coordination needed):** add a namespaced-URL-attribute walk inside `Cleaner.__call__` so the URL-scheme filter doesn\u0027t depend on `link_attrs`. Sketch:\n\n```python\n# lxml_html_clean/clean.py \u2014 supplements rewrite_links() in __call__\n_NS_URL_ATTRS = (\u0027xlink:href\u0027,)  # extend as needed\n_BAD_SCHEME = re.compile(r\u0027^\\s*(javascript|vbscript|data):\u0027, re.I)\n\nfor el in doc.iter():\n    for attr in _NS_URL_ATTRS:\n        if attr in el.attrib and _BAD_SCHEME.match(el.attrib[attr]):\n            del el.attrib[attr]\n```\n\nThis 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.\n\n**Defense in depth (optional, regardless of which fix path is taken):**\n- Also handle `srcset`: the value is a `url 1x, url 2x, \u2026` descriptor list, so split on commas and validate each candidate URL. Not directly executable in current browsers, but closes the same gap.\n- 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.\n\n## Severity\n\nCVSS 3.1 base score: **8.2 / High** \u2014 `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 (\u2192 7.6).\n\nSeverity 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 \u2014 verified at HEAD: default-config `Cleaner()(\u003csvg\u003e\u003ca xlink:href=\"javascript:\u2026\"\u003ex\u003c/a\u003e\u003c/svg\u003e)` \u2192 `\u003csvg\u003e\u003ca\u003ex\u003c/a\u003e\u003c/svg\u003e`.\n\n## Prior art / novelty\n\n- **CVE-2021-28957 (lxml 4.6.3)** \u2014 same root cause, different attribute (`formaction`). Fix was a one-line extension of `link_attrs`. Direct precedent.\n- **CVE-2022-34473** (Mozilla Sanitizer API) \u2014 `xlink:href` URL bypass primitive in a different sanitizer.\n- **Bleach (Mozilla, Python)** explicitly handles the `xlink` namespace; `enshrined/svg-sanitize` (PHP) ships `cleanXlinkHrefs()`; DOMPurify scrubs `xlink:href` via `ALLOWED_URI_REGEXP`.\n- **`nh3`** (the alternative recommended in `lxml_html_clean`\u0027s own README for security-sensitive use) is **not vulnerable** to this primitive \u2014 verified 2026-05-10 on `nh3==0.3.5`: with `\u003csvg\u003e`/`\u003cmath\u003e`/`\u003ca\u003e` and `xlink:href` explicitly added to `tags`/`attributes`, both SVG and MathML payloads, all four scheme-encoding variants, are stripped (output e.g. `\u003csvg\u003e\u003ca rel=\"noopener noreferrer\"\u003ex\u003c/a\u003e\u003c/svg\u003e`).\n\n\n## Coordination\n\nFiling as a private GHSA at `fedora-python/lxml_html_clean` \u2014 `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 \u2014 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\u0027re welcome to coordinate with them directly if you\u0027d prefer the upstream fix over the in-package alternative above.\n\nHappy to provide a draft patch or PR on either path. No bounty expected.",
      "id": "GHSA-4jhm-jv67-739f",
      "modified": "2026-07-08T20:23:22Z",
      "published": "2026-07-08T20:23:22Z",
      "references": [
        {
          "type": "WEB",
          "url": "https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-4jhm-jv67-739f"
        },
        {
          "type": "PACKAGE",
          "url": "https://github.com/fedora-python/lxml_html_clean"
        }
      ],
      "schema_version": "1.4.0",
      "severity": [
        {
          "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
          "type": "CVSS_V3"
        }
      ],
      "summary": "`lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes"
    }

    OPENSUSE-SU-2026:11191-1

    Vulnerability from csaf_opensuse - Published: 2026-07-06 00:00 - Updated: 2026-07-06 00:00
    Summary
    python313-lxml_html_clean-0.4.5-1.1 on GA media
    Severity
    Moderate
    Notes
    Title of the patch: python313-lxml_html_clean-0.4.5-1.1 on GA media
    Description of the patch: These are all security issues fixed in the python313-lxml_html_clean-0.4.5-1.1 package on the GA media of openSUSE Tumbleweed.
    Patchnames: openSUSE-Tumbleweed-2026-11191
    Terms of use: CSAF 2.0 data is provided by SUSE under the Creative Commons License 4.0 with Attribution (CC-BY-4.0).
    Affected products
    Product Identifier Version Remediation
    Unresolved product id: openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.aarch64
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.ppc64le
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.s390x
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.x86_64
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.aarch64
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.ppc64le
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.s390x
    Vendor Fix
    Unresolved product id: openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.x86_64
    Vendor Fix
    Threats
    Impact moderate

    {
      "document": {
        "aggregate_severity": {
          "namespace": "https://www.suse.com/support/security/rating/",
          "text": "moderate"
        },
        "category": "csaf_security_advisory",
        "csaf_version": "2.0",
        "distribution": {
          "text": "Copyright 2024 SUSE LLC. All rights reserved.",
          "tlp": {
            "label": "WHITE",
            "url": "https://www.first.org/tlp/"
          }
        },
        "lang": "en",
        "notes": [
          {
            "category": "summary",
            "text": "python313-lxml_html_clean-0.4.5-1.1 on GA media",
            "title": "Title of the patch"
          },
          {
            "category": "description",
            "text": "These are all security issues fixed in the python313-lxml_html_clean-0.4.5-1.1 package on the GA media of openSUSE Tumbleweed.",
            "title": "Description of the patch"
          },
          {
            "category": "details",
            "text": "openSUSE-Tumbleweed-2026-11191",
            "title": "Patchnames"
          },
          {
            "category": "legal_disclaimer",
            "text": "CSAF 2.0 data is provided by SUSE under the Creative Commons License 4.0 with Attribution (CC-BY-4.0).",
            "title": "Terms of use"
          }
        ],
        "publisher": {
          "category": "vendor",
          "contact_details": "https://www.suse.com/support/security/contact/",
          "name": "SUSE Product Security Team",
          "namespace": "https://www.suse.com/"
        },
        "references": [
          {
            "category": "external",
            "summary": "SUSE ratings",
            "url": "https://www.suse.com/support/security/rating/"
          },
          {
            "category": "self",
            "summary": "URL of this CSAF notice",
            "url": "https://ftp.suse.com/pub/projects/security/csaf/opensuse-su-2026_11191-1.json"
          },
          {
            "category": "self",
            "summary": "SUSE CVE CVE-2026-49825 page",
            "url": "https://www.suse.com/security/cve/CVE-2026-49825/"
          }
        ],
        "title": "python313-lxml_html_clean-0.4.5-1.1 on GA media",
        "tracking": {
          "current_release_date": "2026-07-06T00:00:00Z",
          "generator": {
            "date": "2026-07-06T00:00:00Z",
            "engine": {
              "name": "cve-database.git:bin/generate-csaf.pl",
              "version": "1"
            }
          },
          "id": "openSUSE-SU-2026:11191-1",
          "initial_release_date": "2026-07-06T00:00:00Z",
          "revision_history": [
            {
              "date": "2026-07-06T00:00:00Z",
              "number": "1",
              "summary": "Current version"
            }
          ],
          "status": "final",
          "version": "1"
        }
      },
      "product_tree": {
        "branches": [
          {
            "branches": [
              {
                "branches": [
                  {
                    "category": "product_version",
                    "name": "python313-lxml_html_clean-0.4.5-1.1.aarch64",
                    "product": {
                      "name": "python313-lxml_html_clean-0.4.5-1.1.aarch64",
                      "product_id": "python313-lxml_html_clean-0.4.5-1.1.aarch64"
                    }
                  },
                  {
                    "category": "product_version",
                    "name": "python314-lxml_html_clean-0.4.5-1.1.aarch64",
                    "product": {
                      "name": "python314-lxml_html_clean-0.4.5-1.1.aarch64",
                      "product_id": "python314-lxml_html_clean-0.4.5-1.1.aarch64"
                    }
                  }
                ],
                "category": "architecture",
                "name": "aarch64"
              },
              {
                "branches": [
                  {
                    "category": "product_version",
                    "name": "python313-lxml_html_clean-0.4.5-1.1.ppc64le",
                    "product": {
                      "name": "python313-lxml_html_clean-0.4.5-1.1.ppc64le",
                      "product_id": "python313-lxml_html_clean-0.4.5-1.1.ppc64le"
                    }
                  },
                  {
                    "category": "product_version",
                    "name": "python314-lxml_html_clean-0.4.5-1.1.ppc64le",
                    "product": {
                      "name": "python314-lxml_html_clean-0.4.5-1.1.ppc64le",
                      "product_id": "python314-lxml_html_clean-0.4.5-1.1.ppc64le"
                    }
                  }
                ],
                "category": "architecture",
                "name": "ppc64le"
              },
              {
                "branches": [
                  {
                    "category": "product_version",
                    "name": "python313-lxml_html_clean-0.4.5-1.1.s390x",
                    "product": {
                      "name": "python313-lxml_html_clean-0.4.5-1.1.s390x",
                      "product_id": "python313-lxml_html_clean-0.4.5-1.1.s390x"
                    }
                  },
                  {
                    "category": "product_version",
                    "name": "python314-lxml_html_clean-0.4.5-1.1.s390x",
                    "product": {
                      "name": "python314-lxml_html_clean-0.4.5-1.1.s390x",
                      "product_id": "python314-lxml_html_clean-0.4.5-1.1.s390x"
                    }
                  }
                ],
                "category": "architecture",
                "name": "s390x"
              },
              {
                "branches": [
                  {
                    "category": "product_version",
                    "name": "python313-lxml_html_clean-0.4.5-1.1.x86_64",
                    "product": {
                      "name": "python313-lxml_html_clean-0.4.5-1.1.x86_64",
                      "product_id": "python313-lxml_html_clean-0.4.5-1.1.x86_64"
                    }
                  },
                  {
                    "category": "product_version",
                    "name": "python314-lxml_html_clean-0.4.5-1.1.x86_64",
                    "product": {
                      "name": "python314-lxml_html_clean-0.4.5-1.1.x86_64",
                      "product_id": "python314-lxml_html_clean-0.4.5-1.1.x86_64"
                    }
                  }
                ],
                "category": "architecture",
                "name": "x86_64"
              },
              {
                "branches": [
                  {
                    "category": "product_name",
                    "name": "openSUSE Tumbleweed",
                    "product": {
                      "name": "openSUSE Tumbleweed",
                      "product_id": "openSUSE Tumbleweed",
                      "product_identification_helper": {
                        "cpe": "cpe:/o:opensuse:tumbleweed"
                      }
                    }
                  }
                ],
                "category": "product_family",
                "name": "SUSE Linux Enterprise"
              }
            ],
            "category": "vendor",
            "name": "SUSE"
          }
        ],
        "relationships": [
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python313-lxml_html_clean-0.4.5-1.1.aarch64 as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.aarch64"
            },
            "product_reference": "python313-lxml_html_clean-0.4.5-1.1.aarch64",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python313-lxml_html_clean-0.4.5-1.1.ppc64le as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.ppc64le"
            },
            "product_reference": "python313-lxml_html_clean-0.4.5-1.1.ppc64le",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python313-lxml_html_clean-0.4.5-1.1.s390x as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.s390x"
            },
            "product_reference": "python313-lxml_html_clean-0.4.5-1.1.s390x",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python313-lxml_html_clean-0.4.5-1.1.x86_64 as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.x86_64"
            },
            "product_reference": "python313-lxml_html_clean-0.4.5-1.1.x86_64",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python314-lxml_html_clean-0.4.5-1.1.aarch64 as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.aarch64"
            },
            "product_reference": "python314-lxml_html_clean-0.4.5-1.1.aarch64",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python314-lxml_html_clean-0.4.5-1.1.ppc64le as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.ppc64le"
            },
            "product_reference": "python314-lxml_html_clean-0.4.5-1.1.ppc64le",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python314-lxml_html_clean-0.4.5-1.1.s390x as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.s390x"
            },
            "product_reference": "python314-lxml_html_clean-0.4.5-1.1.s390x",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          },
          {
            "category": "default_component_of",
            "full_product_name": {
              "name": "python314-lxml_html_clean-0.4.5-1.1.x86_64 as component of openSUSE Tumbleweed",
              "product_id": "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.x86_64"
            },
            "product_reference": "python314-lxml_html_clean-0.4.5-1.1.x86_64",
            "relates_to_product_reference": "openSUSE Tumbleweed"
          }
        ]
      },
      "vulnerabilities": [
        {
          "cve": "CVE-2026-49825",
          "ids": [
            {
              "system_name": "SUSE CVE Page",
              "text": "https://www.suse.com/security/cve/CVE-2026-49825"
            }
          ],
          "notes": [
            {
              "category": "general",
              "text": "unknown",
              "title": "CVE description"
            }
          ],
          "product_status": {
            "recommended": [
              "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.aarch64",
              "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.ppc64le",
              "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.s390x",
              "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.x86_64",
              "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.aarch64",
              "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.ppc64le",
              "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.s390x",
              "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.x86_64"
            ]
          },
          "references": [
            {
              "category": "external",
              "summary": "CVE-2026-49825",
              "url": "https://www.suse.com/security/cve/CVE-2026-49825"
            },
            {
              "category": "external",
              "summary": "SUSE Bug 1270285 for CVE-2026-49825",
              "url": "https://bugzilla.suse.com/1270285"
            }
          ],
          "remediations": [
            {
              "category": "vendor_fix",
              "details": "To install this SUSE Security Update use the SUSE recommended installation methods like YaST online_update or \"zypper patch\".\n",
              "product_ids": [
                "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.aarch64",
                "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.ppc64le",
                "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.s390x",
                "openSUSE Tumbleweed:python313-lxml_html_clean-0.4.5-1.1.x86_64",
                "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.aarch64",
                "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.ppc64le",
                "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.s390x",
                "openSUSE Tumbleweed:python314-lxml_html_clean-0.4.5-1.1.x86_64"
              ]
            }
          ],
          "threats": [
            {
              "category": "impact",
              "date": "2026-07-06T00:00:00Z",
              "details": "moderate"
            }
          ],
          "title": "CVE-2026-49825"
        }
      ]
    }

    PYSEC-2026-2614

    Vulnerability from pysec - Published: 2026-07-13 15:46 - Updated: 2026-07-13 16:04
    VLAI
    Details

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

     # 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 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 / HighAV: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_cleanlxml_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.

    Impacted products
    Name purl
    lxml-html-clean pkg:pypi/lxml-html-clean

    {
      "affected": [
        {
          "package": {
            "ecosystem": "PyPI",
            "name": "lxml-html-clean",
            "purl": "pkg:pypi/lxml-html-clean"
          },
          "ranges": [
            {
              "events": [
                {
                  "introduced": "0"
                },
                {
                  "fixed": "0.4.5"
                }
              ],
              "type": "ECOSYSTEM"
            }
          ],
          "versions": [
            "0.1.0",
            "0.1.1",
            "0.2.0",
            "0.2.1",
            "0.2.2",
            "0.3.0",
            "0.3.1",
            "0.4.0",
            "0.4.1",
            "0.4.2",
            "0.4.3",
            "0.4.4"
          ]
        }
      ],
      "aliases": [
        "CVE-2026-49825",
        "GHSA-4jhm-jv67-739f"
      ],
      "details": "# `lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes (`xlink:href`)\n\n**Reporter:** Guillem Lefait \u003cguillem@datamq.com\u003e \u00b7 **Date:** 2026-05-10\n**Affected:** `lxml` \u2264 6.1.0 and `lxml_html_clean` \u2264 0.4.4 (latest stable)\n**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 \u2014 bug is in pure-Python sanitizer logic, independent of the libxml2 backend)\n**Root-cause class:** same as CVE-2021-28957 (`formaction` missing from `link_attrs`)\n\n## Summary\n\n`Cleaner` filters URL schemes (`javascript:`, `vbscript:`, \u2026) 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` \u2014 a documented option for callers that want lenient attribute handling but still expect URL-scheme scrubbing \u2014 `\u003ca xlink:href=\"javascript:\u2026\"\u003e` survives sanitization untouched, and any browser that follows the SVG-anchor specification will execute the JavaScript when the rendered link is clicked.\n\n**CWE:** CWE-79 (XSS), with CWE-184 (Incomplete List of Disallowed Inputs) as the underlying defect class.\n\n## Affected components\n\n| Package            | Versions tested        | File / line                      |\n|--------------------|------------------------|----------------------------------|\n| `lxml`             | 4.9.x, 5.2.1, 6.1.0    | `src/lxml/html/defs.py:20`       |\n| `lxml`             | \"                      | `src/lxml/html/__init__.py:485-528` |\n| `lxml_html_clean`  | 0.4.0 \u2013 0.4.4          | `lxml_html_clean/clean.py:348,576` |\n\nThe legacy `lxml.html.clean` module \u2014 bundled in `lxml \u003c 5.2.0` and still installable on newer versions via the `lxml[html_clean]` extra \u2014 shares the same bug.\n\n## Root cause\n\n`defs.link_attrs` is a flat string set; the literal `xlink:href` is absent:\n\n```python\n# lxml/html/defs.py\nlink_attrs = frozenset([\n    \u0027action\u0027, \u0027archive\u0027, \u0027background\u0027, \u0027cite\u0027, \u0027classid\u0027,\n    \u0027codebase\u0027, \u0027data\u0027, \u0027href\u0027, \u0027longdesc\u0027, \u0027profile\u0027, \u0027src\u0027,\n    \u0027usemap\u0027, \u0027dynsrc\u0027, \u0027lowsrc\u0027, \u0027formaction\u0027,\n])\n```\n\n`HtmlMixin.iterlinks()` (`lxml/html/__init__.py:526-528`) only yields attributes whose key is in that set:\n\n```python\nfor attrib in link_attrs:\n    if attrib in attribs:\n        yield (el, attrib, attribs[attrib], 0)\n```\n\n`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.\n\n## Minimal reproducer\n\n```python\nfrom lxml import html\nfrom lxml_html_clean import Cleaner\n\nfor payload in (\n    \u0027\u003csvg\u003e\u003ca xlink:href=\"javascript:alert(1)\"\u003ex\u003c/a\u003e\u003c/svg\u003e\u0027,\n    \u0027\u003cmath\u003e\u003ca xlink:href=\"javascript:alert(2)\"\u003ey\u003c/a\u003e\u003c/math\u003e\u0027,\n):\n    tree = html.fromstring(payload)\n    Cleaner(safe_attrs_only=False)(tree)\n    print(html.tostring(tree).decode())\n    print(\u0027  iterlinks:\u0027, list(html.fromstring(payload).iterlinks()))\n# \u003csvg\u003e\u003ca xlink:href=\"javascript:alert(1)\"\u003ex\u003c/a\u003e\u003c/svg\u003e     \u2190 unchanged\n#   iterlinks: []                                          \u2190 link rewriter blind\n# \u003cmath\u003e\u003ca xlink:href=\"javascript:alert(2)\"\u003ey\u003c/a\u003e\u003c/math\u003e   \u2190 unchanged\n#   iterlinks: []                                          \u2190 link rewriter blind\n```\n\nBoth SVG and MathML scopes are vulnerable \u2014 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\u0026#x73;cript:`), embedded tab (`java\\tscript:`).\n\n## Impact\n\nA caller that uses `Cleaner` to neutralise untrusted HTML and chooses `safe_attrs_only=False` \u2014 typically because the application wants to allow custom data-/aria-/vendor attributes \u2014 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.\n\n## Suggested fix\n\n**Extend `link_attrs`** to include `xlink:href`. In HTML mode, `lxml.html` keeps prefixed attribute names verbatim \u2014 the parsed key is the literal string `xlink:href`, not a Clark-notation form \u2014 so the existing allow-list lookup is a plain string match. Same shape as the CVE-2021-28957 fix:\n\n```diff\n # lxml/html/defs.py\n link_attrs = frozenset([\n     \u0027action\u0027, \u0027archive\u0027, \u0027background\u0027, \u0027cite\u0027, \u0027classid\u0027,\n     \u0027codebase\u0027, \u0027data\u0027, \u0027href\u0027, \u0027longdesc\u0027, \u0027profile\u0027, \u0027src\u0027,\n     \u0027usemap\u0027, \u0027dynsrc\u0027, \u0027lowsrc\u0027, \u0027formaction\u0027,\n+    \u0027xlink:href\u0027,\n ])\n```\n\nThis single change closes the reported XSS for both SVG `\u003ca xlink:href\u003e` and MathML `\u003ca xlink:href\u003e`. `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.\n\n**Alternative (in-package fix, no lxml coordination needed):** add a namespaced-URL-attribute walk inside `Cleaner.__call__` so the URL-scheme filter doesn\u0027t depend on `link_attrs`. Sketch:\n\n```python\n# lxml_html_clean/clean.py \u2014 supplements rewrite_links() in __call__\n_NS_URL_ATTRS = (\u0027xlink:href\u0027,)  # extend as needed\n_BAD_SCHEME = re.compile(r\u0027^\\s*(javascript|vbscript|data):\u0027, re.I)\n\nfor el in doc.iter():\n    for attr in _NS_URL_ATTRS:\n        if attr in el.attrib and _BAD_SCHEME.match(el.attrib[attr]):\n            del el.attrib[attr]\n```\n\nThis 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.\n\n**Defense in depth (optional, regardless of which fix path is taken):**\n- Also handle `srcset`: the value is a `url 1x, url 2x, \u2026` descriptor list, so split on commas and validate each candidate URL. Not directly executable in current browsers, but closes the same gap.\n- 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.\n\n## Severity\n\nCVSS 3.1 base score: **8.2 / High** \u2014 `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 (\u2192 7.6).\n\nSeverity 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 \u2014 verified at HEAD: default-config `Cleaner()(\u003csvg\u003e\u003ca xlink:href=\"javascript:\u2026\"\u003ex\u003c/a\u003e\u003c/svg\u003e)` \u2192 `\u003csvg\u003e\u003ca\u003ex\u003c/a\u003e\u003c/svg\u003e`.\n\n## Prior art / novelty\n\n- **CVE-2021-28957 (lxml 4.6.3)** \u2014 same root cause, different attribute (`formaction`). Fix was a one-line extension of `link_attrs`. Direct precedent.\n- **CVE-2022-34473** (Mozilla Sanitizer API) \u2014 `xlink:href` URL bypass primitive in a different sanitizer.\n- **Bleach (Mozilla, Python)** explicitly handles the `xlink` namespace; `enshrined/svg-sanitize` (PHP) ships `cleanXlinkHrefs()`; DOMPurify scrubs `xlink:href` via `ALLOWED_URI_REGEXP`.\n- **`nh3`** (the alternative recommended in `lxml_html_clean`\u0027s own README for security-sensitive use) is **not vulnerable** to this primitive \u2014 verified 2026-05-10 on `nh3==0.3.5`: with `\u003csvg\u003e`/`\u003cmath\u003e`/`\u003ca\u003e` and `xlink:href` explicitly added to `tags`/`attributes`, both SVG and MathML payloads, all four scheme-encoding variants, are stripped (output e.g. `\u003csvg\u003e\u003ca rel=\"noopener noreferrer\"\u003ex\u003c/a\u003e\u003c/svg\u003e`).\n\n\n## Coordination\n\nFiling as a private GHSA at `fedora-python/lxml_html_clean` \u2014 `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 \u2014 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\u0027re welcome to coordinate with them directly if you\u0027d prefer the upstream fix over the in-package alternative above.\n\nHappy to provide a draft patch or PR on either path. No bounty expected.",
      "id": "PYSEC-2026-2614",
      "modified": "2026-07-13T16:04:44.674997Z",
      "published": "2026-07-13T15:46:29.614932Z",
      "references": [
        {
          "type": "WEB",
          "url": "https://github.com/fedora-python/lxml_html_clean/security/advisories/GHSA-4jhm-jv67-739f"
        },
        {
          "type": "PACKAGE",
          "url": "https://github.com/fedora-python/lxml_html_clean"
        },
        {
          "type": "PACKAGE",
          "url": "https://pypi.org/project/lxml-html-clean"
        },
        {
          "type": "ADVISORY",
          "url": "https://github.com/advisories/GHSA-4jhm-jv67-739f"
        },
        {
          "type": "ADVISORY",
          "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49825"
        }
      ],
      "severity": [
        {
          "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
          "type": "CVSS_V3"
        }
      ],
      "summary": "`lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes"
    }