ASP.NET のセキュリティ

ASP.NET アプリケーションを保護する

Adam Tuliper

先月号では、Web アプリケーションにセキュリティを組み込むことの重要性と、2 種類の攻撃 (SQL インジェクションとパラメーターの改ざん)、およびその攻撃を防ぐ方法について解説しました (msdn.microsoft.com/magazine/hh580736)。今回はその後編として、さらにもう 2 種類の攻撃、クロスサイト スクリプティング (XSS) とクロスサイト リクエスト フォージェリ (CSRF) を取り上げます。

運用環境でセキュリティ スキャナーを使うだけでよいと思われている方もいらっしゃるでしょう。スキャナーは、簡単な目標物を見つけるのには優れたツールで、アプリケーションとシステムの構成上の問題の検出には特に適しています。ですが、スキャナーは開発者と同じ程度にアプリケーションを熟知することはできません。したがって、開発者が潜在的なセキュリティ問題に詳しくなり、アプリケーションを確認する時間を取って、ソフトウェア開発ライフサイクルにセキュリティ構築を組み込むことは必須です。

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

概要: クロスサイト スクリプティング (XSS) は、ユーザーが認識しないうちに、ユーザーの閲覧セッションに悪意のあるスクリプトを挿入する攻撃です。この攻撃は、ある巨大なソーシャル ネットワーキング サイトが被害に遭い、サイトのユーザーが承認していないメッセージを投稿する事態に陥った事件によって、多くの人々に知られることとなりました。攻撃者が悪意のあるスクリプトをポストしてブラウザーに実行させると、スクリプトは被害者のセッション コンテキストで実行されます。これにより、偽のログイン ダイアログを表示したり、Cookie を盗んだりするなど、実質的に攻撃者が DOM を好き放題に操ることができるようになります。この攻撃では、表示されているページに HTML キー ロガーをインストールして、ウィンドウからリモート サイトに入力を送信し続けるようにすることも可能です。

悪用方法: XSS の悪用方法はいくつかありますが、そのすべてが出力のエスケープの解除か、不当なエスケープによるものです。簡単な状態メッセージをエンド ユーザーに表示するアプリケーションの例について考えてみましょう。通常、このメッセージは、図 1 のようにクエリ文字列に渡されます。

Query String Message
図 1 クエリ文字列メッセージ

この手法はプロフィールが保存されたことを示す図 1 のようなメッセージなど、なんらかの状態をユーザーに示すために、通常リダイレクト後に使用されます。このメッセージは、クエリ文字列から読み取られ、ページに出力されます。ところが、出力が HTML でエンコードされていない場合、だれもが状態メッセージの代わりに JavaScript を挿入できるようになってしまいます。この種類の攻撃は、クエリ文字列の内容がすべてページに表示されるため、リフレクト XSS 攻撃と呼ばれます。永続的な攻撃では、通常、データベースや Cookie に悪意のあるスクリプトが保存されます。

図 1 では、URI が msg パラメーターを受け取っていることがわかります。この URI の Web ページは、エンコードなしに変数を書き込むために、次のようなコードを含んでいます。

<div class="messages"> <%=Request.QueryString["msg"]%></div>

Profile Saved を図 2 のスクリプトに置き換えると、クエリ文字列に含まれているスクリプトによって、ブラウザーに alert 関数が表示されます。この攻撃の重要な点は、結果が HTML でエンコードされていないために、<script> タグがブラウザーによって有効な JavaScript として解析され、実行されてしまうことです。これは、開発者が望むことではないのは明らかです。

Injecting Script into the URI
図 2 URI へのスクリプトの挿入

このように、警告が表示されても、大きな問題ではありません。図 3 に示すように、この例をもっと深く掘り下げてみましょう。趣旨を明確にするために、ここでは攻撃を簡略化しています。この構文は、厳密に言うと正確ではありませんが、少し変更を加えるだけで、実際に機能する攻撃になります。

Creating a Malicious Attack
図 3 悪意のある攻撃の作成

この攻撃は、偽のログイン ダイアログを表示し、ユーザーが特に警戒することなく資格情報を入力するよう仕向けます。この例の場合は、リモート スクリプトがダウンロードされます。リモート スクリプトのダウンロードは、ブラウザーのセキュリティ設定では既定で有効になっている場合がほとんどです。この種類の攻撃は、エンコードや除去が行われていない文字列をユーザーにエコー バックできる箇所では、理論上どこでも行われる可能性があります。

図 4 は、ユーザーが製品についてのコメントを投稿する開発者向けのサイトで、同様のスクリプトを使用している攻撃です。実際の製品レビューではなく、悪意のある JavaScript がコメントとして残されたとします。このスクリプトは、Web ページにアクセスしているあらゆるユーザーにログイン ダイアログを表示して、資格情報を収集し、リモート サイトに送信します。スクリプトはデータベースに保存され、ページを閲覧するあらゆるユーザーで実行されるので、これは永続的な XSS 攻撃であると言えます。

A Persistent XSS Attack Showing a Fake Dialog
図 4 偽のダイアログを表示する永続的な XSS 攻撃

XSS は、動的なテキストが HTML タグで使用可能な場合などに、HTML 要素を用いて悪用されることもあります。以下に例を示します。

<img onmouseover=alert([user supplied text])>

攻撃者が onmouseout=alert(document.cookie) などのテキストを挿入すると、Cookie にアクセスするブラウザーで、次のタグが作成されます。

<img onmouseover=alert(1) onmouseout=alert(document.cookie) >

入力値にフィルターをかけるために <script> タグはなく、何もエスケープするものはありませんが、これは Cookie (認証された Cookie) を読み取ることができる、完全に有効な JavaScript の一部分です。このセキュリティを高める方法は、場合ごとに異なります。ただ、リスクを考慮すると、ユーザー入力がこのインライン コードに到達しないようにすることが最も推奨されます。

XSS から保護する方法: 以下の規則に厳密に従えば、全部とはいかなくても、アプリケーションをほとんどの XSS 攻撃から守ることができます。

  1. すべての出力を HTML でエンコードする。

  2. HTML 要素の属性文字列になるようなユーザー指定のテキストを許可しない。

  3. msdn.microsoft.com/library/3yekbd5b で説明されているように Request.Browser を確認することで、アプリケーションで Internet Explorer 6 が使用されないようにする。

  4. コントロールの動作を確認して、コントロールが出力を HTML でエンコードするかどうかを認識し、エンコードしていない場合はコントロールに送信されるデータをエンコードする。

  5. Microsoft Anti-Cross Site Scripting Library (AntiXSS) を使用し、既定の HTML エンコーダーとして設定する。

  6. HTML データをデータベースに保存する前に AntiXSS Sanitizer オブジェクト (このライブラリは別途ダウンロードする必要があります。後ほど説明します) を使用して GetSafeHtml か GetSafeHtmlFragment を呼び出し、データをエンコードしてから保存する。

  7. Web フォームの場合、Web ページで EnableRequestValidation=false を設定しない。残念ながら、Web で見られるユーザー グループの投稿では、エラーが発生した場合この設定を無効にするよう勧められている場合がほとんどです。この設定には理由があり、たとえば <X という文字の組み合わせがサーバーにポスト バックされると、要求を停止します。コントロールがサーバーに HTML をポスト バックすると図 5 のようなエラーが発生する場合、サーバーにポストする前にデータをエンコードするのが理想です。これは WYSIWYG コントロールに共通のシナリオで、最新のバージョンのほとんどは、サーバーにポスト バックする前に HTML データを適切にエンコードします。

    Server Error from Unencoded HTML
    図 5 エンコードされていない HTML によるサーバー エラー

  8. ASP.NET MVC 3 アプリケーションでは、HTML をモデルにポスト バックする必要がある場合、ValidateInput(false) を使用して要求の検証をオフにしないようにし、以下のように、モデル プロパティに [AllowHtml] を追加する。

public class BlogEntry
{
  public int UserId {get;set;}
  [AllowHtml]
  public string BlogText {get;set;}
}

製品の中には、XSS を検出するために、文字列の中から <script> や他の単語の組み合わせ、または正規表現パターンを検出しようとするものもあります。このような製品を使えば二重チェックを実行できますが、攻撃にはさまざまなバリエーションがあるため、完全に信頼できるわけではありません。ha.ckers.org/xss.html (英語) で XSS のチート シートをご覧になると、検出がいかに難しいかおわかりいただけると思います。

解決策を理解するため例を示します。攻撃者が、以下のクエリ文字列かフォーム フィールドから、変数になるなんらかのスクリプトをアプリケーションに挿入したとしましょう。

string message = Request.QueryString["msg"];

または

string message = txtMessage.Text;

TextBox コントロールは出力を HTML でエンコードしますが、Text プロパティはエンコードしないので、ユーザーはエンコードされていない Text プロパティをコードから読み取ることになります。どちらのコードでも、message 変数に以下の文字列が残ります。

message = "<script>alert('bip')</script>"

次のようなコードを含む Web ページではこのテキストがページに書き込まれるだけで、ユーザーのブラウザーで JavaScript が実行されます。

<%=message %>

出力を HTML でエンコードすることにより、その場でこの攻撃が停止されます。図 6 に、危険なデータをエンコードする主な方法を示します。

これらの方法は、例にあるような攻撃を防ぐので、必ずアプリケーションで使用するようにしてください。

図 6 HTML エンコードの方法

ASP.NET (MVC または Web フォーム) <%=Server.HtmlEncode(message) %>
Web フォーム (ASP.NET 4 構文) <%: message %>
ASP.NET MVC 3 Razor @message
データ バインド

残念ながら、データ バインド構文にはまだエンコード構文が組み込まれていません。次のバージョンの ASP.NET には <%#: %> として組み込まれる予定です。それまでは、次の構文を使用してください。

<%# Server.HtmlEncode(Eval("PropertyName")) %>

より適切なエンコード

Microsoft.Security.Application 名前空間の AntiXSS ライブラリにある以下を使用することをお勧めします。

Encoder.HtmlEncode(message)

使用するコントロールを知るのは重要です。データを HTML でエンコードするコントロールと、エンコードしないコントロールについて確認するようにしてください。たとえば、TextBox コントロールは表示される出力を HTML でエンコードしますが、LiteralControl はエンコードしません。これは重要な区別です。以下のテキストが代入されるテキスト ボックスについて考えてみましょう。

yourTextBoxControl.Text = "Test <script>alert('bip')</script>";

このテキスト ボックスは、次のように、適切にページにテキストを表示します。

Test &lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;

それでは、次のテキスト ボックスはどうでしょう。

yourLiteralControl.Text = "Test <script>alert('bip')</script>";

このテキスト ボックスは、JavaScript の alert がページで表示されるようにするため、XSS のぜい弱性を高めてしまいます。解決方法は次のとおりです。

yourLiteralControl.Text = Server.HtmlEncode(
    "Test <script>alert('bip')</script>");

この手法は、Web フォームでデータ バインドを使用する際には少し複雑です。次の例をご覧ください。

<asp:Repeater ID="Repeater1" runat="server">
    <ItemTemplate>
      <asp:TextBox ID="txtYourField" Text='<%# Bind("YourField") %>'
        runat="server"></asp:TextBox>
    </ItemTemplate>
  </asp:Repeater>

これはぜい弱ではありません。このインライン コードは、スクリプトを出力したり、コントロールの引用符を無視したりするように見えますが、実際にはエンコードされています。

それでは、次の例はどうでしょう。

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%# Eval("YourField") %>
  </ItemTemplate>
</asp:Repeater>

これはぜい弱です。データ バインドの構文 <%# %> は、HTML エンコーディングを行いません。修正案は次のとおりです。

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Eval("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

このシナリオで Bind を使用する場合、前後で Server.HtmlEncode をラップすることはできません。これは、Bind がバックグラウンドで 2 つの異なる呼び出しとしてコンパイルされるためです。ラップすると、次のようにエラーになります。

<asp:Repeater ID="Repeater2" runat="server">
  <ItemTemplate>
    <%#Server.HtmlEncode((string)Bind("YourText"))%>
  </ItemTemplate>
</asp:Repeater>

Bind を使用していて、HTML でエンコードを行うコントロール (TextBox コントロールなど) にテキストが代入されない場合、Eval を使用すれば、前の例のように、Server.HtmlEncode への呼び出しをラップすることができます。

データ バインドと同じ概念は ASP.NET MVC には存在しないため、HTML ヘルパーがエンコードを行うかどうかについて確認する必要があります。ラベルとテキスト ボックスのヘルパーは、HTML でエンコードを行います。たとえば、次のコードについて考えてみてください。

@Html.TextBox("customerName", "<script>alert('bip')</script>")
@Html.Label("<script>alert('bip')</script>")

このコードは、以下のようにレンダリングされます。

<input id="customerName" name="customerName" type="text"
  value="&lt;script>alert(&#39;bip&#39;)&lt;/script>" />
<label for="">&lt;script&gt;alert(&#39;bip&#39;)&lt;/script&gt;</label>

AntiXSS については先ほど触れました。現在バージョン 4.1である AntiXSS Library ベータ 1 は、書き換えにたいへん優れています。また、セキュリティが考慮されている場合は、ASP.NET 付属のものより優れた HTML エンコーダーを提供します。Server.HtmlEncode に問題があるわけではありませんが、焦点がセキュリティではなく互換性に当てられています。AntiXSS のエンコード手法は異なります。詳細については、msdn.microsoft.com/security/aa973814 (英語) を参照してください。

ベータ版は wpl.codeplex.com/releases/view/80289 (英語) からダウンロードできます。AntiXSS がベータ版ではなくなったかどうか確認し、もしベータ版のままの場合は、コードをダウンロードしてコンパイルする必要があります。この詳細については、Jon Galloway が bit.ly/lGpKWX (英語) でわかりやすく説明しています。

AntiXSS エンコーダーを使用するには、以下の呼び出しを行います。

<%@ Import Namespace="Microsoft.Security.Application" %>
...
...
<%= Encoder.HtmlEncode(plainText)%>

ASP.NET MVC 4 には、既定の ASP HTML エンコーダーを上書きして AntiXSS エンコーダーを使用できる、新しい機能が追加されています。この記事の執筆時点では、AntiXSS Library の最新バージョンは 4.1 です。また、現在ベータ版のため、コードをダウンロードしてコンパイルし、アプリケーションへの参照としてライブラリに追加する必要があります。これはすべて 5 分程度で実行できます。その後、web.config の <system.web> セクションに、次のコードを追加します。

<httpRuntime encoderType=
  "Microsoft.Security.Application.AntiXssEncoder, AntiXssLibrary"/>

これで、ASP.NET MVC 3 Razor 構文などの図 6 の構文で行われる HTML エンコーディング呼び出しがすべて、AntiXSS ライブラリによってエンコードされるようになります。これは、プラグ可能な機能としてはどうでしょう。

ライブラリには、データベースに保存する前に HTML を浄化できる Sanitizer オブジェクトも含まれています。これは、HTML の編集のために、ユーザーに WYSIWYG エディターを提供する際に非常に便利です。この呼び出しは、次のように文字列からスクリプトを取り除こうとします。

using Microsoft.Security.Application;
...
...
string wysiwygData = "before <script>alert('bip ')</script> after ";
string cleanData = Sanitizer.GetSafeHtmlFragment(wysiwygData);
This results in the following cleaned string that can then be saved to the database:
cleanData = "before  after ";

クロスサイト リクエスト フォージェリ (CSRF)

概要: クロスサイト リクエスト フォージェリ (CSERF と略され、"シーサーフ" と発音します) は、ブラウザーと Web サイトの間にある信頼関係を利用して、何の悪意もないユーザーのセッションを用いてコマンドを実行する攻撃です。この攻撃は、詳しく説明しないと内容を掴みにくいため、すぐに例に入ることにします。

悪用方法: 例として、John というユーザーが、PureShoppingHeaven サイトの管理者として認証されているとします。PureShoppingHeaven は管理者しかアクセスできない URL を含んでおり、新しいユーザーの作成などの操作を実行するために URL に情報が渡されることを許可します (図 7 参照)。

Passing Information on the URL
図 7 URL への情報の提供

なんらかの方法を使用して John がこの URL を要求するように攻撃者が仕向けると、John のブラウザーはサーバーからそれを要求し、John のブラウザーのキャッシュ済みの認証情報や使用中の認証情報 (例としては、認証 Cookie や Windows 認証などのその他の認証トークンが挙げられます) を送信します。

これは簡単な例ですが、CSRF 攻撃の中にはさらに複雑なものもあり、GET 要求に加えてフォーム POST を組み込んで、同時に XSS などの別の攻撃を利用することも可能です。

John が、悪用されているぜい弱なソーシャル ネットワーキング サイトを閲覧したとします。攻撃者は、AddUser.aspx URL を要求する John のセッションの XSS ぜい弱性を通じて、ページになんらかの JavaScript を配置したことが考えられます。John が Web ページを閲覧した後、Fiddler (fiddler2.com、英語) のこのダンプを参照すると、次のように、ブラウザーがカスタムのサイト認証 Cookie も送信していることがわかります。

GET http://pureshoppingheaven/AddUser.aspx?userName=hacked&pwd=secret HTTP/1.1
Host: pureshoppingheaven
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
Cookie: CUSTOMERAUTHCOOKIE=a465bc0b-e1e2-4052-8292-484d884229ab

これらはすべて、John の知らないところで行われます。理解する必要があるのは、設計上、ブラウザーはあらゆる有効な Cookie や認証情報を送信するようになっているということです。電子メール クライアントは、既定では画像を読み込まないように設定されていることをご存知だと思います。この理由の 1 つは、CSRF を防ぐためです。次のような画像タグが埋め込まれている HTML 形式の電子メールをユーザーが受信した場合、タグの中にある URL が要求されます。サーバーは、Web サイトに対してユーザーが認証されている場合、この要求を行います。

<img src='yoursite/createuser.aspx?id=hacked&pwd=hacked' />

ユーザーが既に認証されている "yoursite" の管理者の場合、ブラウザーは、あらゆる資格情報と共に GET 要求を適切に送信します。サーバーはこれを認証済みのユーザーによる有効な要求として見なし実行します。電子メール クライアントに表示される有効な画像の応答がないため、これはユーザーの知らないうちに行われます。

CSRF から保護する方法: CSERF を防ぐためには、次の規則に従います。

  1. GET 要求のリンクをクリックして、要求を再生できないようにする。GET 要求の HTTP 仕様では、GET 要求は状態の変更ではなく取得のみに使用する必要があることが示されています。
  2. 攻撃者がフォーム POST 要求を模倣するために JavaScript を使用した際、要求を再生できないようにする。
  3. GET を通じた操作をすべて防ぐ。たとえば、URL を通じてレコードの作成または削除が実行されないようにします。実行するためにはユーザーによる操作を必須にすることが理想的です。精巧なフォーム ベースの攻撃を防ぐことはできませんが、例で示した電子メールの画像や、XSS の危険にさらされたサイトに埋め込まれている基本的なリンクなどの、簡単な攻撃のホストは制限できます。

Web フォーム経由の攻撃の防止は、ASP.NET MVC とは少々異なります。Web フォームでは、ViewState MAC 属性を署名できます。これは、EnableViewStateMac=false を設定しない限り、フォージェリの防止策になります。ワンクリック攻撃と呼ばれる攻撃をブロックするために、現在のユーザー セッションで ViewState を署名して、クエリ文字列に ViewState が渡されるのを防ぐこともお勧めします (図 8 参照)。

図 8 ワンクリック攻撃の防止

void Page_Init(object sender, EventArgs e)
{
  if (Session.IsNewSession)
  {
    // Force session to be created;
    // otherwise the session ID changes on every request.
    Session["ForceSession"] = DateTime.Now;
  }
  // 'Sign' the viewstate with the current session.
  this.ViewStateUserKey = Session.SessionID;
  if (Page.EnableViewState)
  {
    // Make sure ViewState wasn't passed on the querystring.
    // This helps prevent one-click attacks.
    if (!string.IsNullOrEmpty(Request.Params["__VIEWSTATE"]) &&
      string.IsNullOrEmpty(Request.Form["__VIEWSTATE"]))
    {
      throw new Exception("Viewstate existed, but not on the form.");
    }
  }
}

ここでランダムなセッション値を割り当てた理由は、セッションが必ず確立されるようにするためです。一時的なセッション ID はどれでも使用することができますが、ASP.NET セッション ID は、実際にセッションを作成するまであらゆる要求において変化します。ここでは、要求ごとに変化するセッション ID は好ましくないため、新しいセッションを作成することで固定する必要があります。

ASP.NET MVC には、要求と共に渡される一意トークンを使用して CSERF を防ぐ、一連のヘルパーが組み込まれています。ヘルパーは、必須である非表示フォーム フィールドに加えて Cookie 値も使用し、要求の偽造をより困難にします。これらの保護は簡単に実装できるので、アプリケーションに必ず組み込むようにしてください。ビューで <form> に @Html.AntiForgery­Token() を追加するには、次のようにします。

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken();
  @Html.EditorForModel();
  <input type="submit" value="Submit" />
}
Decorate any controllers that accept post data with the [Validate­AntiForgeryToken], like so:
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult Index(User user)
{
  ...
}

ぜい弱性について理解する

この記事では、Web アプリケーションがハッキングされる 2 つの一般的な方法、クロスサイト スクリプティングとクロスサイト リクエスト フォージェリを取り上げました。この知識と、先月解説した SQL インジェクションとパラメーターの改ざんに関する知識を合わせ、アプリケーションのぜい弱性について詳しく理解していただけたことと思います。

また、最も一般的な攻撃をいくつか防ぐためにアプリケーションにセキュリティを組み込むのがどれほど簡単であるかおわかりいただけたのではないでしょうか。ソフトウェア開発ライフサイクルに、既にセキュリティ構築を組み込んでいる方は、ぜひこのままセキュリティを重視し続けてください。組み込んでいない方は、すぐに実践することをお勧めします。既存のアプリケーションを、1 ページごと、または 1 モジュールごとに監査することができます。また、ほとんどの場合、それらは非常に簡単にリファクタリングできます。加えて、資格情報の盗難に備え、SSL でアプリケーションを保護してください。開発ライフサイクル中、およびその前後において、セキュリティについて必ず考慮するようにしてください。

Adam Tuliper は、Cegedim 社のソフトウェア アーキテクトで、20 年以上にわたってソフトウェア開発に取り組んでいます。彼は、国内の INETA コミュニティの講演者です。また、会議や .NET ユーザー グループで定期的に講演しています。彼の Twitter は twitter.com/AdamTuliper (英語) から、ブログは completedevelopment.blogspot.com (英語) または secure-coding.com (英語) からご覧いただけます。ASP.NET アプリケーションをハッキングから守ることの詳細については、今度公開される Pluralsight の動画シリーズをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Barry Dorrans に心より感謝いたします。