OWASP Java HTML Sanitizer: Prevent XSS in Spring Boot with Practical Examples
This article demonstrates how to use the OWASP Java HTML Sanitizer library to prevent DOM-based XSS attacks in Spring Boot applications, covering dependency setup, pre-packaged and custom sanitization policies, advanced preprocessors, and change listeners with complete code examples.
Introduction to DOM-Based XSS and the Need for Sanitization
In real-world development, directly inserting unprocessed user input into innerHTML, document.write, or rendering data from URL parameters, localStorage, or API responses as HTML easily triggers DOM-based XSS. These vulnerabilities bypass server-side defenses; Content Security Policy (CSP) cannot block them, and backend filtering is ineffective. Even if script tags are filtered, attackers can use SVG event handlers or javascript: pseudo-protocols to circumvent protections. Therefore, strict sanitization must be applied at every dynamic insertion point.
OWASP Java HTML Sanitizer Overview
The OWASP Java HTML Sanitizer is an open-source library from Google. It is a fast, easily configurable Java HTML cleaner that allows web applications to include third-party HTML while preventing cross-site scripting (XSS) attacks. The sanitizer JAR has zero runtime dependencies; its only compile-time dependency is spotbugs-annotations (provided scope, used only for annotations). Other JARs are required only for the test suite. The code follows security best practices, includes an extensive test suite, and has passed adversarial security reviews.
Dependency Setup
Environment: Spring Boot 3.5.0. Add the following Maven dependency (requires JDK 11 or higher):
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20260924.2</version>
</dependency>Source code is available at https://github.com/OWASP/java-html-sanitizer. To build all Maven modules and run tests, execute ./mvnw clean verify from the repository root. The Maven wrapper locks the required Maven version. When building a specific module with -pl, also use -am to automatically build its dependencies.
Repository Structure
owasp-java-html-sanitizer/ – contains the released library files, corresponding test cases, and the Java module descriptor.
java8-shim/ and java10-shim/ – provide compatibility adaptation code packaged into the final library JAR.
examples/ – offers tested, reusable example filtering policies (not formally released).
empiricism/ – holds browser experiment code and data used to generate the sanitizer's element rule tables.
docs/ – includes user documentation, security-related documents, and historical archives.
Quick Start: Pre-packaged Policies
The library provides ready-made policies such as Sanitizers.FORMATTING and Sanitizers.LINKS. They can be combined using .and().
String untrustedHTML = """
<script>alert("xxxooo")</script>
<h1>Spring Boot3实战案例300讲<script>document.wirte("xxxooo")</script></h1>
""";
PolicyFactory policy = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
String safeHTML = policy.sanitize(untrustedHTML);
System.err.println(safeHTML);Output: Spring Boot3实战案例300讲 The malicious script tags are stripped, leaving only safe formatting and link elements.
Custom Policies with HtmlPolicyBuilder
Allow Specific Elements and Attributes
Define a policy that permits only a elements with https URLs and enforces rel="nofollow":
String untrustedHTML = """
<script>alert("xxxooo")</script>
<h1>Spring Boot3实战案例300讲<script>document.wirte("xxxooo")</script></h1>
<a href="javascript:alert('hello')">你好</a>
""";
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("a")
.allowUrlProtocols("https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.toFactory();
String safeHTML = policy.sanitize(untrustedHTML);
System.err.println(safeHTML);Output:
Spring Boot3实战案例300讲
你好The script tags and the javascript: protocol link are removed; only the plain text of the link remains.
Element Transformation
Convert heading tags ( h1 – h6) into div elements with a custom class:
String untrustedHTML = """
<script>alert("xxxooo")</script>
<h1>Spring Boot3实战案例300讲<script>document.wirte("xxxooo")</script></h1>
<a href="javascript:alert('hello')">你好</a>
""";
PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("p")
.allowElements(
(String elementName, List<String> attrs) -> {
// Add class attribute
attrs.add("class");
attrs.add("header-" + elementName);
// Return the element name to include; empty string deletes it.
return "div";
}, "h1", "h2", "h3", "h4", "h5", "h6")
.toFactory();
String safeHTML = policy.sanitize(untrustedHTML);
System.err.println(safeHTML);Output:
<div class="header-h1">Spring Boot3实战案例300讲</div>
你好The h1 becomes a div with class header-h1; scripts and the unsafe link are dropped.
Attribute Policies with Custom Logic
Allow data-foo attributes on div and span with a custom matcher:
PolicyFactory myPolicy = new HtmlPolicyBuilder()
.allowElements("div", "span")
.allowAttributes("data-foo")
.matching(
(String elementName, String attributeName, String value) -> {
// Return the attribute value, or null to delete.
return value;
})
.onElements("div", "span")
.toFactory();If an element like a, font, img, input, or span should be allowed even when it has no attributes, explicitly whitelist it with allowWithoutAttributes(). Adding attribute policies does not weaken default checks (e.g., style or URL attribute validation).
Advanced Features
Preprocessors
Preprocessors enable insertion of text and large-scale structural changes before policy application. They do not affect output safety because they run prior to policy enforcement.
Example: Append the origin (scheme + authority) of each link's URL in parentheses after the link text.
String untrustedHTML = """
<script>alert("xxxooo")</script>
<h1>Spring Boot3实战案例300讲<script>document.wirte("xxxooo")</script></h1>
<a href="javascript:alert('hello')">你好</a>
""";
PolicyFactory myPolicy = new HtmlPolicyBuilder()
.withPreprocessor(
(HtmlStreamEventReceiver r) -> {
return new HtmlStreamEventReceiverWrapper(r) {
@Override
public void openTag(String elementName, List<String> attrs) {
if ("a".equals(elementName)) {
for (int i = 0, n = attrs.size(); i < n; i += 2) {
if ("href".equals(attrs.get(i))) {
String url = attrs.get(i + 1);
String origin;
try {
URI uri = new URI(url);
String scheme = uri.getScheme();
String authority = uri.getRawAuthority();
if (scheme == null && authority == null) {
origin = null;
} else {
origin = (scheme != null ? scheme + ":" : "")
+ (authority != null ? "//" + authority : "");
}
} catch (URISyntaxException ex) {
origin = "about:invalid";
}
if (origin != null) {
text(" (" + origin + ") ");
}
}
}
}
super.openTag(elementName, attrs);
}
};
})
.allowElements("a")
.allowAttributes("href").onElements("a")
.allowStandardUrlProtocols()
.toFactory();
String safeHTML = myPolicy.sanitize(untrustedHTML);
System.err.println(safeHTML);Output:
Spring Boot3实战案例300讲
(javascript:) 你好The preprocessor adds (javascript:) after the link text before the policy sanitizes the javascript: protocol (which is later removed by allowStandardUrlProtocols()).
Change Listeners
When a policy rejects an element or attribute, it notifies an HtmlChangeListener. This can be used to track policy violations and detect attempts to bypass security controls.
String untrustedHTML = """
<script>alert("xxxooo")</script>
<h1>Spring Boot3实战案例300讲<script>document.wirte("xxxooo")</script></h1>
<a href="javascript:alert('hello')">你好</a>
""";
PolicyFactory policy = ...;
String safeHTML = policy.sanitize(untrustedHTML, new HtmlChangeListener<Object>() {
@Override
public void discardedTag(Object context, String elementName) {
System.err.println(elementName);
}
@Override
public void discardedAttributes(Object context, String elementName, String... attributeNames) {
System.err.println("%s, %s".formatted(elementName, Arrays.toString(attributeNames)));
}
}, Object.class);
System.err.println(safeHTML);Output:
script
script
a
<div class="header-h1">Spring Boot3实战案例300讲</div>
你好The listener logs each discarded tag ( script, script, a) and the final sanitized HTML. When an element is dropped because all its attributes were rejected (e.g., a link whose sole href was removed), discardedAttributes is also triggered. Two additional default methods provide more detail: discardedAttribute receives the value of each rejected attribute, and discardedText receives tag-like content removed from retained literal elements or content the renderer could not output.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
