Cross-site scripting (XSS)

Cross-site scripting in Java Servlet and JSP HTML responses

Description

Cross-site scripting occurs when untrusted data is inserted into an HTML response without protection appropriate to its output context. An attacker can alter markup or execute JavaScript in the response's origin, perform account actions, read sensitive information, or display a phishing interface.

HTML text, HTML attributes, URLs, JavaScript, and CSS have different parsers and syntax. Reusing an encoder for the wrong context, calling a helper merely named sanitize, or removing selected characters does not establish safety. Apply the correct output encoding at the point where the value is used. Use a reviewed HTML sanitizer only when users need to author a limited set of markup.

Recommended remediation

Prefer auto-escaped templates over assembling HTML strings directly. Keep JSTL <c:out>'s default escapeXml behavior, and use Thymeleaf th:text or [[...]]. Restrict unescaped output such as escapeXml="false", th:utext, and [(...)] to reviewed rich HTML.

Output context Recommended handling Caution
HTML text Encode.forHtml or Encode.forHtmlContent Do not reuse it unchanged for attributes, JavaScript, CSS, or URLs.
Quoted ordinary HTML attribute Encode.forHtmlAttribute Always quote values. Event handlers, URLs, style, and srcdoc need separate policies.
Unavoidable legacy unquoted ordinary attribute Add quotes first; use Encode.forHtmlUnquotedAttribute only when conversion is not possible. The official API discourages this use; the application must control the delimiter following the value.
Path or query component of an application-owned URL Encode.forUriComponent This does not validate an entire untrusted URL. Do not substitute deprecated forUri.
Entire untrusted URL Parse and normalize it, allow-list the scheme (normally https) and required hosts, then apply forHtmlAttribute. Do not validate hosts with a string-prefix check alone.
Quoted string inside <script> Encode.forJavaScriptBlock Do not insert data as code, identifiers, or template structure. Do not use forJavaScriptSource, which is unsafe within HTML.
Quoted string in an event handler Encode.forJavaScriptAttribute Prefer removing inline event handlers.
Quoted CSS string Encode.forCssString within a fixed property structure Quote the CSS string and do not let input control property names or stylesheet structure.
CSS url(...) value Validate the URL or construct an application-owned URL, then use Encode.forCssUrl. forCssUrl does not validate the scheme or host.

Apache Commons Text escapeHtml4 does not encode apostrophes, so it is not a general remedy for single-quoted attributes. Use Spring HtmlUtils.htmlEscape and legacy encoders only after confirming their exact output context.

When rich HTML is required

If markup is unnecessary, encode the value as text instead of sanitizing it. Otherwise, use a current patched release at deployment and review a minimal application-owned allow policy. The releases checked on 2026-08-30 were OWASP Java HTML Sanitizer 20260313.1, jsoup 1.23.2, and DOMPurify 3.4.14.

  • On the server, use a maintained OWASP Java HTML Sanitizer or a reviewed jsoup Safelist. Do not mutate shared policies or manipulate sanitized output afterward.
  • For rich HTML in browsers, use a maintained DOMPurify release. Native Element.setHTML() is still not a Baseline feature; use it only after confirming support in the required runtimes.
  • Do not use setHTMLUnsafe() as the standard remedy.

CSP, X-Content-Type-Options: nosniff, explicit text/html;charset=UTF-8, and HttpOnly/Secure/SameSite cookies provide defense in depth. They do not replace context-specific output protection.

Examples

Before

The request values are inserted directly into HTML text and an entire URL attribute.

java
import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public final class ProfileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        final PrintWriter out = response.getWriter();

        String bio = request.getParameter("bio");
        String next = request.getParameter("next");
        out.println("<div class=\"bio\">" + bio + "</div>");
        out.println("<a href=\"" + next + "\">계속</a>");
    }
}

After

HTML text and ordinary attributes are encoded separately. The URL includes only one encoded component within a fixed application-owned path.

java
import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.owasp.encoder.Encode;

public final class SafeProfileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("X-Content-Type-Options", "nosniff");
        final PrintWriter out = response.getWriter();

        String bio = request.getParameter("bio");
        String label = request.getParameter("label");
        String profileId = request.getParameter("profileId");

        out.println("<div class=\"bio\">" + Encode.forHtml(bio) + "</div>");
        out.println("<span title=\"" + Encode.forHtmlAttribute(label) + "\">프로필</span>");
        out.println("<a href=\"/profiles/" + Encode.forUriComponent(profileId) +
            "\">계속</a>");
    }
}

If users need to author some markup, use a policy-based sanitizer rather than attempting to preserve tags through encoding.

java
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;

public final class RichHtmlSanitizer {
    private static final PolicyFactory POLICY =
        Sanitizers.FORMATTING.and(Sanitizers.LINKS);

    public static String sanitize(String untrustedMarkup) {
        return POLICY.sanitize(untrustedMarkup);
    }
}

References