説明
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ではないため、一般的な互換性対策とはみなさないでください。
例
変更前
コントローラーのレスポンス
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のレスポンス
using Microsoft.AspNetCore.Mvc;
app.MapGet("/preview", ([FromQuery] string html) =>
Results.Content(html, "text/html"));
変更後
推奨: Razorの通常のエンコード出力
using Microsoft.AspNetCore.Mvc;
public sealed class PreviewController : Controller
{
[HttpGet("/preview-name")]
public IActionResult Preview([FromQuery] string name)
{
return View("Preview", model: name);
}
}
<h1>@Model</h1>
Razorの通常の文字列出力はHTMLエンコードされます。同じ値にHtml.Raw(Model)を使うと、この保護が失われます。
推奨: マークアップが不要なレスポンス
public IActionResult Preview([FromQuery] string text)
{
return Content(text, "text/plain");
}
限定的な用途: HTMLテキストノードのエンコード
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でサニタイズできます。
const cleanHtml = DOMPurify.sanitize(untrustedHtml);
preview.innerHTML = cleanHtml;
DOMPurifyを最新に保ち、後続のテンプレート処理や文字列処理が結果を変更しないようにしてください。サーバー側のサニタイザーを選ぶ場合も、保守状況、許可する要素と属性、URLプロトコル、対象のHTMLコンテキストを確認します。