CWE-80: Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS) - Java
Overview
CWE-80 is the tag-injection case of XSS: <, > and & reach the page without
being converted to entities, so an attacker's <script> or
<img onerror=...> is parsed as markup rather than shown as text.
The fix is the output encoding described on
CWE-79 - Java, which is where the encoders live:
the OWASP Java Encoder (Encode.forHtml, forHtmlAttribute, forJavaScript,
forUriComponent), JSTL <c:out>, Thymeleaf and JSF escaping, JSON
serialisation for API responses, and AntiSamy or the OWASP Java HTML Sanitizer
for content that must keep real markup. This page does not repeat them.
What belongs here is the failure mode that produces CWE-80 findings specifically - trying to remove the dangerous tags instead of encoding the characters that make them tags.
Common Vulnerable Patterns
Stripping tags with a blocklist
// VULNERABLE - removing what looks dangerous, rather than encoding
public static String clean(String input) {
String out = input.replaceAll("(?i)<script>", "");
out = out.replace("javascript:", "");
return out;
}
out.println("<div>" + clean(request.getParameter("comment")) + "</div>");
Why this is vulnerable: every one of these survives it.
<img src=x onerror=alert(1)> no <script> tag at all
<svg onload=alert(1)> same, different element
<scr<script>ipt>alert(1)</script> the inner match is removed, leaving <script>
javascript:alert(1) the literal "javascript:" never appears
The set of ways to execute script is open-ended and grows with the HTML and
browser specifications, so a blocklist is guessing against a list it cannot
enumerate. The replace is worse than useless: removing javascript: once
turns javajavascript:script: into javascript:.
Escaping switched off for the filtered value
<%-- VULNERABLE - escapeXml="false" trusts the filter's output as markup --%>
<c:out value="${comment}" escapeXml="false" />
<%-- VULNERABLE - EL interpolation does no escaping at all --%>
<div>${comment}</div>
Why this is vulnerable: <c:out> escapes by default, and
escapeXml="false" turns that off. Plain ${...} interpolation in a JSP never
escaped in the first place. Thymeleaf's th:utext and JSF's
escape="false" are the same decision in other templating engines: each
asserts the value is trusted markup, which is only true if something upstream
guaranteed it - and a blocklist filter does not.
Secure Patterns
Encode, and leave the template engine alone
<c:out value="${comment}" /> and Thymeleaf's th:text are already correct for
element content, and Encode.forHtml(value) is the equivalent when writing to
a PrintWriter by hand. The worked examples, including the attribute,
JavaScript, URL and CSS contexts, are on
CWE-79 - Java.
Encoding is what makes the tag-injection case safe: once < is < there is
no tag left to blocklist, whatever the attacker writes.
When the input really is HTML
If users are meant to submit formatting, encoding it would defeat the feature. Use a policy-driven sanitizer, which parses the HTML and rebuilds it from the elements you permit rather than deleting what looks bad:
// SECURE - allowlist parse-and-rebuild, not a blocklist filter
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
private static final PolicyFactory POLICY = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "ul", "ol", "li", "a", "code")
.allowAttributes("href", "title").onElements("a")
.allowUrlProtocols("http", "https", "mailto")
.toFactory();
String safeHtml = POLICY.sanitize(dirtyHtml);
Why this works: the sanitizer parses the input into a document and
re-serialises it from an allowlist, so anything not explicitly permitted cannot
survive. The open-ended set of ways to run script stops mattering, because the
question is no longer "is this dangerous" but "is this on the list".
allowUrlProtocols is what makes the href usable at all. Measured on
owasp-java-html-sanitizer 20240325.1, a builder with no protocol set rejects
every URL, so <a href="https://example.test/"> loses its href along with
javascript:alert(1). Naming http, https and mailto admits those three
and leaves javascript: rejected.
Maven: com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer.
AntiSamy is the policy-file alternative and is covered on
CWE-79 - Java.
Considerations
Whether the finding is CWE-80 or CWE-79 rarely changes the fix. Both are resolved by encoding for the context the value lands in. The label tells you what the scanner matched - tag characters in element content - not the shape of the remediation.
It does change where to look next. CWE-80 only describes element content.
An application that failed to encode there has usually failed in the contexts
CWE-80 does not name: quoted and unquoted attributes, href and src values,
inline <script> blocks, and DOM sinks. Those are CWE-79's territory, and one
CWE-80 instance is a reason to check them.
JSP is where this concentrates. A servlet writing markup with PrintWriter
and a JSP using bare ${...} both bypass every escaping default the stack
offers, so a codebase with one CWE-80 finding in a JSP usually has more. Search
for escapeXml="false", th:utext, escape="false" and ${ outside
<c:out> rather than fixing only the reported line.
A filter kept for other reasons is not the control. Teams sometimes strip tags for content-policy or display reasons and then treat it as the security fix. If the filter stays, encoding still has to be what protects the page.
Additional Resources
- CWE-80: Improper Neutralization of Script-Related HTML Tags
- CWE-79 - Java - the encoders, framework escaping and sanitizer configuration this page defers to
- OWASP Java HTML Sanitizer
- OWASP XSS Prevention Cheat Sheet