Skip to content

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>
java&#115;cript: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 &lt; 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