ASP.NET CoreのHTMLレスポンスにおけるXSS

ASP.NET Coreでリクエストの文字列を直接HTMLとして返す反射型XSS

説明

ASP.NET CoreのControllerBase.Content、Results.Content、TypedResults.Contentは、受け取った文字列をHTMLエンコードしません。リクエストの文字列を直接text/htmlで返すと、ブラウザーはデータではなくマークアップとして解析します。攻撃者がタグ、イベントハンドラーなど実行可能な構文を注入し、反射型クロスサイトスクリプティング(XSS)を起こすおそれがあります。

入力検証と出力エンコードは別の制御です。形式の検証は攻撃面を減らせますが、HTML、属性、URL、JavaScript、CSSそれぞれに必要な出力処理の代わりにはなりません。Sanitize、Encode、Cleanという名前の独自関数も、実装と出力先を確認するまでは安全と判断できません。

想定される影響

  • セッションや機密情報の窃取
  • フィッシング画面の挿入やページ内容の改ざん
  • 被害者の権限によるリクエストやクライアント側の操作
  • 信頼されたサイトのオリジンを使った悪意あるコンテンツの配信

対処方法

生のHTMLを組み立てず、Razorの通常のエンコード出力を優先してください。通常の@valueはHTML向けに文字列をエンコードします。信頼できない値にHtml.Raw、HtmlString、その他のIHtmlContentを使って、この保護を回避しないでください。

マークアップが不要ならtext/plainや構造化データを返してください。HTMLテキストノードに入れる場合は、最終出力時に値全体をWebUtility.HtmlEncodeかHtmlEncoder.Default.Encodeでエンコードします。URL、JavaScript、CSSには、それぞれに合ったエンコーダーや安全なデータ受け渡しを使ってください。JavaScriptへの直接連結より、エンコードしたHTMLデータ属性やJSONで渡し、textContentなど安全な出力先で利用する方法を優先します。

利用者がリッチHTMLを書く必要がある場合、通常のエンコードでは書式を保持できません。レンダリング直前に、その環境に合った保守中の許可リスト型HTMLサニタイザーを使ってください。ブラウザーでは最新のDOMPurifyが選択肢になります。挿入先に合う設定で処理し、結果を挿入前に再結合・変更しないでください。Element.setHTML()はすべての対象環境で使えるBaseline APIではないため、一般的な互換性対策とはみなさないでください。

例

変更前

コントローラーのレスポンス

csharp
using Microsoft.AspNetCore.Mvc;

public sealed class PreviewController : ControllerBase
{
    [HttpGet("/preview")]
    public IActionResult Preview([FromQuery] string html)
    {
        return Content(html, "text/html");
    }
}

htmlはリクエストから直接バインドされ、Contentは値をエンコードしません。

Minimal APIのレスポンス

csharp
using Microsoft.AspNetCore.Mvc;

app.MapGet("/preview", ([FromQuery] string html) =>
    Results.Content(html, "text/html"));

変更後

推奨: Razorの通常のエンコード出力

csharp
using Microsoft.AspNetCore.Mvc;

public sealed class PreviewController : Controller
{
    [HttpGet("/preview-name")]
    public IActionResult Preview([FromQuery] string name)
    {
        return View("Preview", model: name);
    }
}
html
<h1>@Model</h1>

Razorの通常の文字列出力はHTMLエンコードされます。同じ値にHtml.Raw(Model)を使うと、この保護が失われます。

推奨: マークアップが不要なレスポンス

csharp
public IActionResult Preview([FromQuery] string text)
{
    return Content(text, "text/plain");
}

限定的な用途: HTMLテキストノードのエンコード

csharp
using System.Text.Encodings.Web;

public IActionResult Preview([FromQuery] string name)
{
    var encodedName = HtmlEncoder.Default.Encode(name);
    return Content($"<h1>{encodedName}</h1>", "text/html");
}

HTMLテキストノードだけを対象とする例です。同じ結果を<script>、スタイル、URL、イベントハンドラー属性に入れても、安全性は保証されません。

リッチHTMLが必要な場合

サーバーは内容をデータとして渡し、ブラウザーで挿入する直前に最新のDOMPurifyでサニタイズできます。

javascript
const cleanHtml = DOMPurify.sanitize(untrustedHtml);
preview.innerHTML = cleanHtml;

DOMPurifyを最新に保ち、後続のテンプレート処理や文字列処理が結果を変更しないようにしてください。サーバー側のサニタイザーを選ぶ場合も、保守状況、許可する要素と属性、URLプロトコル、対象のHTMLコンテキストを確認します。

参考資料