ASP.NET Core でクロスサイト スクリプティング (XSS) を防ぐ

作成者: Rick Anderson

クロスサイト スクリプティング (XSS) はセキュリティの脆弱性であり、攻撃者がクライアント側のスクリプト (通常は JavaScript) を Web ページに配置できるようにします。 他のユーザーが影響を受けたページを読み込むと、サイバー攻撃のスクリプトが実行されます。 サイバー攻撃者は、Cookie とセッション トークンを盗んだり、DOM 操作を使用して Web ページの内容を変更したり、ブラウザーを別のページにリダイレクトしたりできます。 XSS の脆弱性は、通常、アプリケーションがユーザー入力を受け取り、検証、エンコード、エスケープせずにページに出力するときに発生します。

この記事は、主にビューを含む ASP.NET Core MVC、Razor Pages、および XSS に対して脆弱な HTML を返すその他のアプリに適用されます。 HTML、XML、または JSON の形式でデータを返す Web API は、ユーザー入力を適切にサニタイズしないと、クライアント アプリで XSS 攻撃をトリガーする可能性があります。 この動作は、クライアント アプリが API にどの程度信頼しているかによって異なります。 API がユーザー生成コンテンツを受け入れて HTML 応答で返す場合、データは攻撃を受け入れられます。 サイバー攻撃によって、ユーザーのブラウザーで応答がレンダリングされたときに実行されるコンテンツに悪意のあるスクリプトが挿入される可能性があります。

XSS 攻撃を防ぐには、Web API では入力検証と出力エンコードを実装する必要があります。 入力検証により、ユーザー入力が想定される基準を満たしており、悪意のあるコードが含まれていないことが保証されます。 出力エンコードにより、API によって返されるすべてのデータが適切にサニタイズされるため、ユーザーのブラウザーでコードとして実行できなくなります。 詳細については、GitHub dotnet/aspnetcore.docs の問題 #28789 を参照してください。

XSS からアプリケーションを保護する

基本的なレベルの XSS では、アプリケーションを欺いて、レンダリングされたページに <script> タグを挿入したり、On* イベントを要素に挿入したりします。

XSS をアプリケーションに導入しないようにするには、開発者は次の防止手法を実装する必要があります。

  • このセクションに記載されている他の手法に従わない限り、信頼されていないデータを HTML 入力に入れてはいけません。

    信頼されていないデータは、サイバー攻撃によって制御可能なデータです。 たとえば、HTML フォームの入力、クエリ文字列、HTTP ヘッダー、データベースからソース化されたデータなどがあります。 サイバー攻撃者は、アプリケーションを侵害できない場合でも、データベースを侵害する可能性があります。

  • 信頼されていないデータを HTML 要素に配置する前に、データが HTML エンコードされていることを確認します。

    HTML エンコードは、左山括弧、つまり 小なり<)のような文字を、(&lt;)のような安全な形式に変換します。

  • 信頼されていないデータを HTML 属性に格納する前に、データが HTML 属性でエンコードされていることを確認します。

    この特殊な形式の HTML エンコードでは、二重引用符 (")、一重引用符 (')、アンパサンド (&)、および小さい (<) 文字が処理されます。 信頼されていない入力を処理する場合は、一般的な HTML コンテンツに HTML エンコードを使用し、HTML 属性の HTML 属性エンコードを使用します。

  • 信頼されていないデータを JavaScript に格納する前に、実行時にコンテンツを取得する HTML 要素にデータを配置します。

    この手法に従えない場合は、データが JavaScript でエンコードされていることを確認してください。 JavaScript エンコードは、JavaScript の危険な文字を 16 進数の等価値に変換します。 たとえば、JavaScript エンコードでは、より小さい (<) 文字が 16 進値 \u003Cに変更されます。

  • 信頼されていないデータを URL クエリ文字列に格納する前に、データが URL エンコードされていることを確認します。

で HTML エンコードを調べる Razor

MVC で使用される Razor エンジンは、この動作を回避する作業を行わない限り、変数からソースされたすべての出力を自動的にエンコードします。 at symbol @ ディレクティブを使用するたびに、HTML 属性エンコード規則が使用されます。 HTML 属性エンコードは HTML エンコードのスーパーセットであるため、HTML エンコードと HTML 属性エンコードのどちらを使用するかを考慮する必要はありません。 信頼されていない入力を JavaScript に直接挿入しようとしたときではなく、HTML コンテキストで at シンボル @ のみを使用するようにする必要があります。 Razor タグ ヘルパーは、タグ パラメーターで使用する入力もエンコードします。

次の Razor ビューについて考えてみましょう。

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

このビューは、 untrustedInput 変数の内容を出力します。 この変数には、XSS 攻撃で使用されるいくつかの文字、小なり (<)、二重引用符 (")、および右山括弧または 大なり (>) が含まれます。 ソースを調べると、次のようにエンコードされた出力が表示されます。

&lt;&quot;123&quot;&gt;

警告

ASP.NET Core MVC は、出力時に自動的にエンコードされない HtmlString クラスを提供します。 このクラスは、XSS の脆弱性を公開するため、信頼されていない入力と組み合わせて使用しないでください。

で JavaScript エンコードを調べる Razor

場合によっては、JavaScript に値を挿入してビューで処理することが必要な場合があります。 このタスクを実行するには、2 つの方法があります。 値を入れる最も安全な方法は、タグのデータ属性に値を置いて、JavaScript で取得することです。 次に例を示します。

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or, you can use createElement() to dynamically create document elements.
    // This instance uses textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

上のマークアップでは、次の HTML が生成されます。

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or, you can use createElement() to dynamically create document elements.
    // This instance uses textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

上のコードを実行すると、次の出力が生成されます。

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

警告

JavaScript で信頼されていない入力を連結して DOM 要素を作成したり、動的に生成されたコンテンツに document.write() を使用したりしないでください。

代わりに、次のいずれかの方法を使用して、コードが DOM ベースの XSS に公開されないようにします。

  • createElement()を呼び出し、適切なメソッドまたはプロパティ (node.textContent=node.InnerText=など) を使用してプロパティ値を割り当てます。
  • document.CreateTextNode() メソッドを呼び出し、適切な DOM の場所に追加します。
  • element.SetAttribute() メソッドを呼び出します。
  • element[attribute]=割り当てを使用します。

コード内のエンコーダーにアクセスする

HTML、JavaScript、および URL エンコーダーは、次の 2 つの方法でコードで使用できます。

  • 依存関係の挿入を介してそれらを挿入する。
  • System.Text.Encodings.Web 名前空間に含まれる既定のエンコーダーを使用する。

既定のエンコーダーを使用すると、文字範囲に適用されるカスタマイズ (安全として扱われる) は有効になりません。 既定のエンコーダーでは、可能な限り安全なエンコード規則が使用されます。

依存関係の挿入を介して構成可能なエンコーダーを使用するには、コンストラクターは必要に応じて、 HtmlEncoderJavaScriptEncoderUrlEncoder パラメーターを受け取る必要があります。

次に例を示します。

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

URL パラメーターをエンコードする

信頼されていない入力を値として使用して URL クエリ文字列を作成する場合は、 UrlEncoder パラメーターを使用して値をエンコードします。

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

エンコード後、 encodedValue 変数には文字列 %22Quoted%20Value%20with%20spaces%20and%20%26%22が含まれます。 スペース、引用符、句読点、およびその他の安全でない文字は、16 進数の値にパーセントエンコードされます。 たとえば、空白文字は %20に変換されます。

警告

URL パスの一部として信頼できない入力を使わないでください。 信頼できない入力を常にクエリ文字列値として渡します。

エンコーダーをカスタマイズする

既定では、エンコーダーは Basic Latin Unicode 範囲に限定されたセーフ リストを使用します。 指定された範囲外のすべての文字は、対応する文字コードとしてエンコードされます。 この動作は、エンコーダーを使用して文字列を出力するため、 Razor タグ ヘルパーと HTML ヘルパーによるレンダリングにも影響します。

この動作の目的は、不明または将来のブラウザーのバグから保護することです。 以前のブラウザーのバグでは、英語以外の文字の処理に基づいて解析が中断されました。 Web サイトで中国語、キリル文字などのラテン文字以外の文字を大量に使用する場合、この動作は構成に適していない可能性があります。

エンコーダー セーフ リストをカスタマイズして、起動時にアプリに適した Unicode 範囲を含めることができます。 Program.cs ファイルでカスタマイズを行います。

たとえば、次の HTML のような Razor HTML ヘルパーで既定の構成を使用できます。

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

上記のマークアップは、中国語のテキストがエンコードされた状態でレンダリングされます。

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

エンコーダーによって安全として扱われる文字の範囲を広げるには、 Program.cs ファイルに次の行を挿入します。

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

エンコーダーのセーフ リストをカスタマイズして、起動時に ConfigureServices() でアプリケーションに適した Unicode 範囲を含めることができます。

たとえば、既定の構成を使う場合は、次のように Razor HtmlHelper を使います。

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

Web ページのソースを表示すると、次のように、中国語のテキストがエンコードされた状態で表示されます。

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

エンコーダーによって安全なものとして扱われる文字を拡張するには、ConfigureServices()startup.cs メソッドに次の行を挿入します。

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

次の使用例は、Unicode 範囲 CJK 統合 Ideograph を含むようにセーフ リストを拡大します。 次の出力は、より広い範囲の安全な文字のレンダリングされたビューを示しています。

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

セーフ リストの範囲は、言語ではなく、Unicode コード テーブルとして指定されます。 Unicode 標準には、文字を含むグラフを検索するために使用できるコード グラフの一覧があります。 各エンコーダー (HTML、JavaScript、URL) は個別に構成する必要があります。

セーフ リストのカスタマイズは、依存関係の挿入によって提供されるエンコーダーにのみ影響します。 System.Text.Encodings.Web.*Encoder.Default経由でエンコーダーに直接アクセスする場合は、既定のセーフ リスト (Basic Latin) のみが使用されます。

エンコードするタイミングと場所を決定する

一般に、エンコードは出力の時点で行われ、エンコードされた値をデータベースに格納しないでください。

出力時点でのエンコードを使用すると、データの使用を変更できます。 たとえば、HTML からクエリ文字列値に変更します。 この方法を使用すると、検索前に値をエンコードしなくても、データを簡単に検索できます。 また、エンコーダーに加えられた変更やバグ修正を利用することもできます。

XSS 防止手法として検証を使用する

検証は、XSS 攻撃を最小限に抑える上で役に立つツールです。 たとえば、0 から 9 文字のみを含む数値文字列では、XSS 攻撃はトリガーされません。

ユーザー入力で HTML が受け入れられると、検証がより複雑になります。 HTML 入力の解析は困難であり、場合によっては不可能な場合があります。 Markdown は、埋め込まれた HTML を取り除くパーサーと組み合わせることで、リッチな入力を受け入れるためのより安全な選択肢となります。

検証だけに依存しないようにしてください。 どのような検証またはサニタイズが実行されても、出力前に信頼されていない入力を常にエンコードします。