クロスサイトスクリプティング(XSS)

Java ServletとJSPのHTML応答におけるクロスサイトスクリプティング

説明

クロスサイトスクリプティングは、信頼できないデータを、出力先のコンテキストに必要な保護なしでHTML応答へ挿入した場合に起こります。攻撃者はマークアップを変更したり、その応答のオリジンでJavaScriptを実行したりして、アカウントの操作、機密情報の取得、フィッシング画面の表示などを行うおそれがあります。

HTMLテキスト、HTML属性、URL、JavaScript、CSSは、それぞれ異なるパーサーと構文を使います。別のコンテキスト用のエンコーダーを流用したり、名前が sanitize のヘルパーを呼んだり、一部の文字を削除したりするだけでは安全になりません。値を使う最後の段階で、正しい出力エンコーディングを適用してください。HTML sanitizerは、ユーザーが限定されたマークアップを作成する必要がある場合に使ってください。

推奨する対処方法

HTML文字列を直接組み立てるよりも、自動エスケープされるテンプレートを使用してください。JSTLの <c:out> は既定の escapeXml を維持し、Thymeleafでは th:text または [[...]] を使用してください。escapeXml="false"、th:utext、[(...)] などの非エスケープ出力は、確認済みのリッチHTMLに限定してください。

出力位置 推奨する処理 注意点
HTMLテキスト Encode.forHtml または Encode.forHtmlContent 属性、JavaScript、CSS、URLにそのまま流用しないでください。
引用符で囲んだ通常のHTML属性 Encode.forHtmlAttribute 値を必ず引用符で囲み、イベント、URL、style、srcdoc には別のポリシーを適用してください。
避けられない既存の引用符なし属性 まず引用符を付け、変更できない場合に限り Encode.forHtmlUnquotedAttribute を使用 公式APIもこの使用を推奨していません。値の後の区切りをアプリケーション側で管理してください。
アプリケーション所有URLのパス・クエリ要素 Encode.forUriComponent 信頼できないURL全体を検証する関数ではありません。非推奨の forUri で代用しないでください。
信頼できないURL全体 解析・正規化し、スキーム(通常は https)と必要なホストを許可リストで検証してから forHtmlAttribute を適用 文字列の前方一致だけでホストを検証しないでください。
<script> 内の引用符付き文字列 Encode.forJavaScriptBlock データを実行コード、識別子、テンプレート構造として挿入せず、HTML内では安全でない forJavaScriptSource を使わないでください。
イベントハンドラー内の引用符付き文字列 Encode.forJavaScriptAttribute 可能であればインラインイベントハンドラー自体を削除してください。
引用符で囲んだCSS文字列 固定のプロパティ構造内で Encode.forCssString CSS文字列の引用符が必要です。プロパティ名やスタイルシート構造を入力に制御させないでください。
CSSの url(...) 値 URLを検証するか、アプリケーション所有のURLを作ってから Encode.forCssUrl を適用 forCssUrl 自体はスキームやホストを検証しません。

Apache Commons Textの escapeHtml4 はアポストロフィをエンコードしないため、単一引用符の属性に対する一般的な修正方法にはできません。Springの HtmlUtils.htmlEscape や古いエンコーダーも、正確な対象コンテキストを確認してから使用してください。

リッチHTMLが必要な場合

マークアップが不要なら、sanitizerではなくテキストとしてエンコードしてください。必要な場合は、デプロイ時点で修正済みのバージョンを使い、アプリケーションが管理する最小限の許可ポリシーを確認してください。2026-08-30に確認したリリースは、OWASP Java HTML Sanitizer 20260313.1、jsoup 1.23.2、DOMPurify 3.4.14 です。

  • サーバー側では、更新されたOWASP Java HTML Sanitizerまたは確認済みのjsoup Safelist を使用してください。共有ポリシーを変更したり、無害化後の結果を再び文字列操作したりしないでください。
  • ブラウザー側のリッチHTMLには、更新されたDOMPurifyを使用してください。ネイティブの Element.setHTML() は引き続きBaseline機能ではないため、必要な実行環境の対応を確認してから使用してください。
  • setHTMLUnsafe() を標準の修正方法にしないでください。

CSP、X-Content-Type-Options: nosniff、明示的な text/html;charset=UTF-8、HttpOnly・Secure・SameSite Cookieは多層防御として有用ですが、コンテキストに合った出力保護の代わりにはなりません。

例

変更前

リクエスト値をHTMLテキストとURL全体の属性へ直接挿入しています。

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>");
    }
}

変更後

HTMLテキストと通常の属性をそれぞれエンコードし、URLにはアプリケーションが管理する固定パスの中の一つの要素だけを挿入しています。

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>");
    }
}

ユーザーが一部のマークアップを作成する必要がある場合は、エンコーディングでタグを残そうとせず、ポリシーに基づくsanitizerを使ってください。

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);
    }
}

参考資料