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

作成者: Rick Anderson

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

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

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

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

基本的なレベルの XSS では、アプリケーションを欺いて、レンダリングされたページに <script> タグを挿入したり、On* イベントを要素に挿入したりします。 開発者は、次の防止手順を使って、XSS がアプリケーションに取り込まれないようにする必要があります。

  1. 次の手順を踏まない限り、信頼できないデータを HTML 入力に入れないでください。 信頼できないデータとは、HTML フォーム入力、クエリ文字列、HTTP ヘッダー、さらにはデータベースから取得したデータなど、攻撃者に制御される可能性があるすべてのデータのことです。攻撃者は、アプリケーションに侵入できなくても、データベースに侵入できる可能性があるからです。

  2. HTML 要素内に信頼できないデータを置く前に、HTML エンコードされるようにします。 HTML エンコードは < などの文字を受け取り、< のように安全な形式に変更します

  3. 信頼できないデータを HTML 属性に置く前に、HTML エンコードされるようにします。 HTML 属性エンコードは、HTML エンコードのスーパーセットであり、" や ' などの追加の文字をエンコードします。

  4. 信頼できないデータを JavaScript に入れる前に、実行時に取得する内容を持つ HTML 要素にデータを置きます。 これが不可能な場合は、データが JavaScript でエンコードされるようにします。 JavaScript エンコードでは、JavaScript にとって危険な文字が 16 進数で置き換えられます。たとえば、< は \u003C としてエンコードされます。

  5. 信頼できないデータを URL クエリ文字列に含める前に、URL エンコードされるようにします。

Razor を使った HTML エンコード

MVC で使われる Razor エンジンでは、あえてそのような処理を回避するように取り組まない限り、変数から得られるすべての出力が自動的にエンコードされます。 @ ディレクティブを使うと、常に HTML 属性エンコード規則が使われます。 HTML 属性エンコードは HTML エンコードのスーパーセットなので、HTML エンコードと HTML 属性エンコードのどちらを使う必要があるかについて気にする必要はありません。 信頼できない入力を JavaScript に直接入れようとするときではなく、HTML コンテキストでのみ @ を使う必要があります。 タグ ヘルパーでは、タグ パラメーターで使われる入力もエンコードされます。

次の Razor ビューを開きます。

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

@untrustedInput

このビューには、untrustedInput 変数の内容が出力されます。 この変数には、XSS 攻撃で使われるいくつかの文字、すなわち、<、"、> が含まれています。 ソースを調べると、次のようにエンコードされた出力が表示されます。

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

警告

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

Razor を使った JavaScript エンコード

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 time we're using 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 time we're using 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 名前空間に含まれる既定のエンコーダーを使用する。

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

DI を介して構成可能なエンコーダーを使うには、必要に応じて 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 パスの一部として信頼できない入力を使わないでください。 信頼できない入力を常にクエリ文字列値として渡します。

エンコーダーのカスタマイズ

既定では、エンコーダーは基本ラテン文字 Unicode の範囲に限定されたセーフ リストを使い、その範囲外のすべての文字を同等の文字コードとしてエンコードします。 この動作は、エンコーダーを使って文字列を出力するため、Razor TagHelper と HtmlHelper のレンダリングにも影響します。

この理由は、未知あるいは今後のブラウザーのバグから保護するためのものです (これまでのブラウザーのバグでは、英語以外の文字を処理する際に解析がうまくいかないことがありました)。 Web サイトで、中国語、キリル語などの非ラテン文字を多用している場合、これはおそらく望ましい動作ではありません。

エンコーダーのセーフ リストは、起動時にアプリに適した Unicode 範囲を含むように Program.cs 内でカスタマイズできます。

たとえば、次のような Razor HtmlHelper を使った既定の構成を使う場合があります。

<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>

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

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

この例では、セーフ リストを拡張して、CjkUnifiedIdeographs という Unicode 範囲を追加しています。 レンダリングされた出力は次のようになります

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

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

Note

セーフ リストのカスタマイズは、DI を介して取得されるエンコーダーにのみ影響します。 System.Text.Encodings.Web.*Encoder.Default を介してエンコーダーに直接アクセスした場合は、既定の基本ラテン文字のみのセーフ リストが使われます。

エンコードが行われる場所

一般的に受け入れられている方法は、エンコードが出力時に行われ、エンコードされた値をデータベースに格納すべきではないというものです。 出力の時点でエンコードを行うことで、データの使用方法、たとえば HTML からクエリ文字列の値までを変更することができます。 また、検索の前に値をエンコードしなくてもデータを簡単に検索でき、エンコーダーに加えられた変更やバグ修正を利用できます。

XSS 防止技術としての検証

検証は、XSS 攻撃を最小限に抑える上で役に立つツールです。 たとえば、文字 0-9 のみを含む数値文字列では、XSS 攻撃はトリガーされません。 ユーザー入力で HTML を受け入れると、検証がより複雑になります。 HTML 入力の解析は、不可能ではないにしても困難です。 Markdown は、埋め込まれた HTML を取り除くパーサーと組み合わせることで、リッチな入力を受け入れるためのより安全な選択肢となります。 検証だけに依存しないようにしてください。 検証やサニタイズが実行されているかどうかにかかわらず、信頼できない入力については常に出力する前にエンコードします。