# `lxml_html_clean.Cleaner` does not strip `javascript:` URLs from namespaced URL attributes

lxml_html_clean Cleaner with safe_attrs_only=False fails to strip javascript: URLs on xlink:href, allowing stored XSS via SVG/MathML anchors.

- Payout: Research
- Severity: high
- Program: lxml_html_clean
- Found by: glefait, frenzymadness
- Disclosed: 2026-07-08
- CVE: CVE-2026-49825
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4jhm-jv67-739f
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4jhm-jv67-739f
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:misconfiguration vuln:xss

**Target:** Python HTML sanitizer (Cleaner) URL-scheme filter
**Root cause:** Cleaner's URL-scheme scrubbing delegates to iterlinks(), which only yields attributes in defs.link_attrs; that allow-list omits xlink:href, so the javascript: filter never runs on it.
**Steps:**
1. Parse `<svg><a xlink:href="javascript:alert(1)">x</a></svg>` with `html.fromstring`.
2. Run `Cleaner(safe_attrs_only=False)(tree)`.
3. Serialize: output still contains `xlink:href="javascript:alert(1)"`; `iterlinks()` returns `[]`.
4. Repeat with MathML `<math><a xlink:href=...>` and encoding variants `JaVaScRiPt:`, `javascript:`, `java\tscript:` — all survive.
**How found:** Noticed the same root-cause class as CVE-2021-28957 (formaction missing from link_attrs) and checked whether other URL-bearing attributes, notably namespaced xlink:href, were absent from the allow-list; confirmed iterlinks() yields nothing for the payload.
**Exploitation:** Attacker submits untrusted HTML containing an SVG/MathML anchor with xlink:href=javascript:; app sanitizes with Cleaner(safe_attrs_only=False); payload persists and executes in the rendering origin when a victim clicks the anchor.
**Insight:** Sanitizer URL-scheme filters keyed off a flat attribute allow-list miss namespaced/prefixed attributes (xlink:href) and descriptor lists (srcset); grep the allow-list against every attribute browsers treat as navigable, including SVG/MathML scopes.

## Proof of concept (copied exactly from the original report)

**Python reproducer with SVG/MathML xlink:href payloads**

```
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()))
```

**iterlinks loop that skips xlink:href**

```
for attrib in link_attrs:
    if attrib in attribs:
        yield (el, attrib, attribs[attrib], 0)
```

