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.
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.
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.
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
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP Java Encoder
- OWASP Java Encoder 1.4.0 API
- OWASP Java HTML Sanitizer releases
- jsoup releases
- DOMPurify guidance
- HTML Standard: safe and unsafe dynamic markup insertion
- MDN: Element.setHTML()
- Thymeleaf 3.1 tutorial
- Jakarta Tags 3.0 specification
- Jakarta Servlet 6.1
- OWASP ASVS 5.0.0
- CWE-79: Improper Neutralization of Input During Web Page Generation
- OWASP Top 10:2025 A05 Injection
- OWASP Top 10:2021 A03 Injection