Aug 7, 2026·7 min read·1 visit
A parser differential between HTML5 specifications and Java's string-trimming rules in jsoup versions 1.14.3 to 1.23.0 allows unauthenticated attackers to bypass HTML sanitizers and execute arbitrary scripts in a victim's browser context.
jsoup is a widely used Java library for working with real-world HTML. Versions 1.14.3 up to but excluding 1.23.1 contain a Cross-Site Scripting (XSS) vulnerability. When an application configures a custom Safelist that explicitly permits certain raw-text or RCDATA elements, such as style, title, or iframe, an attacker can exploit a parser-browser desynchronization flaw to bypass sanitization. This is achieved by utilizing trailing ASCII control characters that are handled differently by the HTML5 parsing specification and Java's string normalization methods, resulting in unescaped markup execution on the client side.
The jsoup library is a core security dependency in many Java web applications, serving as the primary validation boundary to sanitize user-submitted HTML before persistence or display. The sanitizer relies on a structured node traversal of the parsed Document Object Model (DOM) to strip unapproved tags and attributes based on a defined Safelist. However, when a security mechanism depends on an intermediate parser parsing and neutralizing input, any discrepancy between the parser's logic and the client-side browser's engine can introduce severe vulnerabilities.
CVE-2026-71497 highlights a critical parser-browser desynchronization flaw in jsoup versions ranging from 1.14.3 up to 1.23.0. The vulnerability is classified under CWE-79 (Improper Neutralization of Input During Web Page Generation) and manifests when applications allow raw-text container elements (such as style, title, or iframe) in their custom Safelist configurations. By appending specific trailing control characters to permitted tag names, an attacker can disrupt the tokenizer's state transitions without failing the final tag name lookup.
The resulting parsed DOM representation remains out of sync with the underlying tree-structure rules. The subsequent serialization process outputs unescaped HTML characters that the client-side browser's HTML5 parser interprets as executable DOM elements. This allows the attacker to smuggle malicious script blocks past the jsoup sanitizer and execute arbitrary JavaScript in the context of the user's active session.
The root cause of CVE-2026-71497 lies in a fundamental behavioral discrepancy between the HTML5 parsing specification's definition of whitespace and the character-stripping logic of Java's native java.lang.String.trim() method. This discrepancy allows malformed element tags to undergo unequal sanitization and tokenization steps.
Under the HTML5 tokenizer standard, only five specific characters are recognized as valid whitespace or delimiters to terminate a tag name: Tab (U+0009), Newline (U+000A), Form Feed (U+000C), Carriage Return (U+000D), and Space (U+0020). Any other ASCII control character—including those under U+0020, such as the unit separator (U+001E) or start of heading (U+0001)—is parsed as a valid constituent character of the tag name itself. Consequently, when jsoup's Tokeniser encounters <style\u001E>, it registers the literal tag name as "style\u001E".
Because the exact tokenized string "style\u001E" is not an exact match for the predefined tag "style", the parser does not transition into the RAWTEXT state. It remains in the default Data state, which treats subsequent elements like < and > as standard HTML entities, decoding them into literal < and > characters within the text nodes. However, when resolving the Tag object for the parsed element, jsoup historically invoked Tag.valueOf() which normalization-trimmed the tag name using String.trim(). Since String.trim() strips all ASCII characters less than or equal to U+0020, the trailing control character U+001E was removed, normalizing "style\u001E" to "style". This discrepancy approved the element under the custom Safelist while preserving decoded executable code inside its text child nodes.
The technical vulnerability was remediated in jsoup version 1.23.1. The primary structural change prevents the parser from executing unsafe trimming operations on internal tokenizer-supplied tag names. Reviewing the patch in TagSet.java reveals how the unconditional trimming was restricted to only execute on raw public API entries.
File: src/main/java/org/jsoup/parser/TagSet.java
@@ -137,7 +137,7 @@ private void doAdd(Tag tag) {
Tag valueOf(String tagName, @Nullable String normalName, String namespace, boolean preserveTagCase) {
Validate.notNull(tagName);
Validate.notNull(namespace);
- tagName = tagName.trim();
+ if (normalName == null) tagName = tagName.trim(); // public API input; tokenizer names are already delimited
Validate.notEmpty(tagName);
Tag tag = get(tagName, namespace);
if (tag != null) return tag;In addition to the changes in TagSet.java, Token.java was refactored to eliminate dependency on ParseSettings.normalName(). The previous implementation in ParseSettings.normalName() used normalization steps that ultimately performed trimming, masking the presence of control characters. In the patched code, normalName is resolved using a simple lowercasing routine, preserving the trailing control character in the token throughout the tokenization and tree-building sequence.
File: src/main/java/org/jsoup/parser/Token.java
@@ -312,7 +314,7 @@ final void appendTagName(String append) {
// might have null chars - need to replace with null replacement character
append = append.replace(TokeniserState.nullChar, Tokeniser.replacementChar);
tagName.append(append);
- normalName = ParseSettings.normalName(tagName.value());
+ normalName = lowerCase(tagName.value());
}This remediation ensures the fix is complete. By retaining the control characters inside the parsed token's name (e.g., keeping it as "style\u001E" instead of normalizing it to "style"), the resulting DOM node is associated with an unknown, custom tag. When the Jsoup Cleaner processes this element, it checks the unknown tag against the permitted safelist. Since "style\u001E" is not defined in any custom safelist, the entire tag and its malicious payload are completely stripped from the final cleaned DOM.
To construct a successful exploit payload against CVE-2026-71497, the attacker must target an application that validates HTML utilizing a custom Safelist configured to accept raw-text container elements. In this scenario, we use the style tag to demonstrate the exploit mechanics.
An attacker crafts a payload utilizing the Unicode unit separator control character (represented as \u001E or \x1E) at the boundary of the style tag:
<style\u001E></style><script>alert(document.domain)</script></style\u001E>The following code snippet illustrates the vulnerable application processing this input payload:
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
public class JsoupBypassPoC {
public static void main(String[] args) {
// Define a custom Safelist that allows raw-text style tags
Safelist customSafelist = Safelist.none().addTags("style");
// The payload uses the \u001E control character directly following 'style'
String payload = "<style\u001E></style><script>alert(document.domain)</script></style\u001E>";
// Process and sanitize the payload using the vulnerable jsoup library
String sanitizedHtml = Jsoup.clean(payload, customSafelist);
// The output contains unescaped, active script blocks
System.out.println("Sanitized Output: " + sanitizedHtml);
}
}Upon execution, jsoup's parser registers the tag as style\u001E, stays in the Data state, and decodes the entity tokens </style><script>alert(document.domain)</script>. During DOM lookup, the name is trimmed to style, matching the safelist rule. When jsoup's serializer formats the approved style node, it emits the children as raw, unescaped text. The output is serialized as <style></style><script>alert(document.domain)</script></style>. A victim's browser parsing this serialized HTML terminates the empty style block and immediately executes the script block.
The primary impact of CVE-2026-71497 is unauthenticated Cross-Site Scripting (XSS). An attacker can execute arbitrary script code within the context of a victim's browser session. Depending on the target web application, this script execution can lead to session hijacking via cookie theft, data exfiltration, localized browser storage access, or DOM hijacking to capture user credentials.
This vulnerability is scored as a CVSS v3.1 base score of 4.7 (Medium). The exploitability is limited by high Attack Complexity (AC:H) because the vulnerability cannot be exploited in default configurations. It requires the targeted application to explicitly implement a custom Safelist that permits raw-text elements like style, title, or iframe.
While the baseline score is Medium, the impact is classified as Low for confidentiality and integrity under standard CVSS criteria, but the practical severity is heavily dependent on application deployment. For instance, applications that allow users to submit custom CSS or inline frames to personalize UI elements are at higher risk. To date, this vulnerability has not been added to CISA's Known Exploited Vulnerabilities catalog, and exploit maturity remains at the proof-of-concept level.
To remediate CVE-2026-71497, organizations should upgrade their jsoup dependencies to version 1.23.1 or newer. This version completely resolves the tag name normalization mismatch between the tokenizer and the DOM tree builder.
For Maven projects, update the dependency configuration block within your pom.xml:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.23.1</version>
</dependency>For Gradle projects, update the implementation line within your build.gradle file:
implementation 'org.jsoup:jsoup:1.23.1'If upgrading is not immediately possible, implement temporary mitigation strategies. First, review custom Safelist configurations and remove permissions for elements that alter parser state (such as style, title, and iframe). Second, implement a strict Content Security Policy (CSP) that enforces script-src directives with nonces or SHA hashes, and explicitly disables unsafe-inline. This serves as a secondary barrier to block execution of script tags even if a sanitizer bypass occurs.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
jsoup jsoup | >= 1.14.3, < 1.23.1 | 1.23.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-site Scripting) |
| Attack Vector | Network (AV:N) |
| CVSS Base Score | 4.7 (Medium) |
| EPSS Score | 0.00043 |
| Exploit Status | Proof-of-Concept (PoC) |
| CISA KEV Status | Not Listed |
The product does not neutralize or incorrectly neutralizes user-controlled input before it is placed in output that is used as a web page that is served to other users.
The ngx-extended-pdf-viewer library embeds a version of Mozilla's pdf.js that contains vulnerability CVE-2026-16633. This vulnerability allows arbitrary JavaScript execution (XSS) upon rendering a malicious PDF file.
A denial-of-service vulnerability in node-re2 prior to version 1.25.1 allows attackers to trigger uncatchable native assertion failures in the Google V8 engine. By supplying output-amplifying replacement templates, an attacker can exceed V8 string limits, resulting in an immediate process crash.
A medium-severity out-of-bounds (OOB) heap read vulnerability exists in node-re2 prior to version 1.26.1. When a raw binary Node.js Buffer with a truncated multi-byte UTF-8 character at its end is passed to the C++ native addon, the internal lookahead routine getUtf8CharSize() over-reads up to 3 bytes from the heap, leading to memory disclosure.
A critical OS command injection vulnerability exists in PHP_CodeSniffer's VCS blame report modules (Gitblame, Hgblame, Svnblame). Due to inadequate escaping of filenames passed to shell execution wrappers like popen(), an attacker who commits a file with a maliciously crafted name can execute arbitrary commands when the victim generates a blame report.
An authenticated stored Cross-Site Scripting (XSS) vulnerability exists in the Control Panel helper of Craft CMS before version 5.10.8. Due to lack of HTML entity encoding within the elementLabelHtml method, unescaped draft names are rendered directly into administrative interfaces.
A path traversal vulnerability exists in the local filesystem driver of Craft CMS. Due to validation occurring before path normalization, directory containment checks can be bypassed by utilizing specific protocol schemes like 'file://' along with directory traversal sequences. This allows authenticated users with administrative privileges to access or manipulate files outside the defined storage root directory.