December 2011

Volume 26 Number 12

ASP.NET のセキュリティ - ASP.NET アプリケーションをハッキングから守る

Adam Tuliper | December 2011

毎日のように、また新たなサイトがハッキングされたというニュースがメディアを賑わせています。このように、優秀なハッカー グループが繰り返し起こす不正アクセスについて耳にするたび、彼らはいったいどれほど高度な技術を使っているのか疑問に思われるかもしれません。最新の攻撃の中にはきわめて複雑なものもありますが、多くの場合、最も効果が大きい攻撃は、非常に単純で、もう何年も使用され続けています。さいわい、このような攻撃から保護するのは通常驚くほど簡単です。

今回から 2 回にわたって、最もよく見受けられるハッキング攻撃をいくつか見ていきます。第 1 回目の今回は、SQL インジェクションとパラメーターの改ざんを取り上げます。来年の 1 月に予定している第 2 回は、クロスサイト スクリプトと、クロスサイト リクエスト フォージェリを扱う予定です。

ハッキングについて考慮すべきなのは、規模の大きいサイトだけではありません。すべての開発者が、自分のアプリケーションへのハッキング行為について考慮する必要があります。アプリケーションのユーザーは、アプリケーションを保護するのは開発者の仕事だと考えています。大量の自動ハッキング プログラムが存在しているため、どんなに小さなアプリケーションであっても、インターネットに公開されていれば、ハッカーの標的になる可能性があります。ユーザー テーブルや顧客テーブルが盗まれ、それらのテーブルのパスワードが別のアプリケーションにも使用されていると想像してみてください。もちろん、ユーザーにはアプリケーションごとにパスワードを変えることを推奨していますが、実践できていないのが実情です。自身のアプリケーションが情報盗難の原因になることを望む開発者はいません。私の身の回りでも、エベレスト旅行について綴った友人のとても小さなブログがハッキングされ、明らかな理由なしに、すべて削除されるという出来事がありました。つまり、アプリケーションは、保護しない限り、安全ではありません。

ネットワークと外部通信機器を物理的に切り離しておかない限り、攻撃者はさまざまな問題点を利用してネットワークにホップする可能性があります。利用される問題点には、プロキシ構成の問題、リモート デスクトップ プロトコル (RDP) または仮想プライベート ネットワーク (VPN) による攻撃、Web ページに単純にアクセスしている社内ユーザーが、リモートでコードを実行できる脆弱性、パスワードの推測、不適切なファイアウォール規則、Wi-Fi (Wi-Fi のセキュリティのほとんどは、近くの駐車場にいる攻撃者によって破られる可能性があります)、機密情報をユーザーに自ら提供させるソーシャル エンジニアリングの手法などがあります。どの環境でも、外部の世界から完全に切り離されていない限り、完ぺきに安全であると見なすことはできません。

ハッキングの脅威は非常に身近なものであり、自身のアプリケーションもハッカーの標的になる可能性があることを理解していただいたところで、このようなハッキングの詳細と、ハッキングから保護する方法について説明します。

SQL インジェクション

概要: SQL インジェクションとは、1 つ以上のコマンドをクエリに挿入し、開発者がまったく意図していない新しいクエリを作成する攻撃です。これは、動的 SQL を使用している場合、つまり、SQL ステートメントを作成するためにコードで文字列を連結しているときは、ほぼ必ず行われる攻撃です。SQL インジェクションは、クエリ、またはプロシージャ呼び出しを作成している場合に Microsoft .NET Framework コードで発生する可能性があります。また、ストアド プロシージャ内で動的 SQL を使用している場合など、サーバー側の T-SQL コードでも発生する可能性があります。

SQL インジェクションは、クエリやデータ編集だけでなく、コマンドの実行がデータベース ユーザーやデータベース サービス アカウントの権限のみに限定されているデータベース コマンドの実行にも使用されるため、特に危険です。SQL Server が管理者アカウントで実行するように構成されており、アプリケーションのユーザーが sysadmin ロールに属している場合は、特に注意が必要です。SQL インジェクションを使用する攻撃は、以下のような目的でシステム コマンドを実行します。

  • バックドアを設ける
  • ポート 80 経由でデータベース全体を転送する
  • パスワードやその他の機密情報を盗むためにネットワーク スニファーをインストールする
  • パスワードを解読する
  • 社内ネットワークを列挙する (別のコンピューターのポートのスキャンも含む)
  • ファイルをダウンロードする
  • プログラムを実行する
  • ファイルを削除する
  • ボットネットの一部になる
  • システムに保存されているオートコンプリートのパスワードをクエリする
  • 新しいユーザーを作成する
  • データの作成、削除、編集およびテーブルの作成、削除を行う

これは一部に過ぎません。危険性は、適切な権限を設定することによってのみ緩和されます。攻撃者の創造性によっては、危険が大きくなります。

SQL インジェクションは昔から存在するため、今でも懸念対象なのかどうか、よくたずねられます。答えは、もちろん懸念対象で、攻撃者は非常に頻繁にこの攻撃を使用します。実際のところ、サービス拒否 (DoS) 攻撃を除けば、SQL インジェクションが最もよく使用される攻撃です。

悪用方法: 通常、攻撃者は SQL インジェクションを悪用して、Web ページに直接進入したり、パラメーターを改ざんしたりします。パラメーターの改ざんで変更されるのは、フォームや URI だけではありません。保護されていない SQL ステートメントでアプリケーションが Cookie、ヘッダーなどの値を使用している場合、それらの値も標的になります (後ほど詳しく説明します)。

フォームの改ざんによる SQL インジェクションの例について見てみましょう。このシナリオは、運用コードで何度も見たことがあります。コードの内容がまったく同じになることはないかもしれませんが、これは、開発者がログイン資格情報を確認するために一般的に使用する方法です。

以下に、ユーザー ログインを取得するために、動的に作成される SQL ステートメントを示します。

string loginSql = string.Format("select * from users where loginid= '{0}

  ' and password= '{1} '"", txtLoginId.Text, txtPassword.Text);

この結果、以下の SQL ステートメントが作成されます。

select * from dbo.users where loginid='Administrator' and  

    password='12345'

これ自体は問題ありません。しかし、フォーム フィールドに、図 1 のように入力されるとします。

有効なユーザー名ではない、悪意のある入力
図 1 有効なユーザー名ではない、悪意のある入力

結果、以下の SQL ステートメントが作成されます。

select * from dbo.users where loginid='anything' union select top 1 *

  from users --' and password='12345'

この例では、"anything" という、不明のログイン ID が挿入されます。これ自体は何のレコードも返しませんが、その後、これらの結果をデータベースの最初のレコードと結合します。さらに、その次にある "--" は、残りのクエリ全体をコメント アウトするので以降のクエリは無視されます。攻撃者は、ログインできるだけでなく、有効なユーザー名がまったくわからなくても、有効なユーザーのレコードを呼び出し側のコードに返すことができます。

すべての開発者が、これとまったく同じシナリオのコードをそのまま使用するわけでないのは明らかですが、ここで重要な点は、アプリケーションを分析する際に、クエリに含める値を通常どこから (以下に例を示します) 取得するかを考える必要があるということです。

  • フォーム フィールド
  • URL パラメーター
  • データベースに保存されている値
  • Cookie
  • ヘッダー
  • ファイル
  • 分離ストレージ

なぜ重要なのか、わかりにくいものもあるかもしれません。たとえば、ヘッダーが問題になる可能性がある理由は、アプリケーションがヘッダーにユーザー プロファイル情報を保存していて、その値を動的クエリで使用すると脆弱性が生まれるためです。動的 SQL を使用する場合は、上記がすべて攻撃の原因となり得ます。

検索機能を備えた Web ページは、攻撃者の格好の標的です。というのも、直接インジェクションを試みることができるためです。

脆弱なアプリケーションは、クエリ エディターのような機能を攻撃者に与えることになります。

ここ数年で、セキュリティの問題について人々の関心が大きく高まったので、システムは既定で強化されることが多くなっています。たとえば、xp_cmdshell システム プロシージャは、SQL Server 2005 以降 (SQL Express 含む) のインスタンスでは無効になっています。だからと言って、攻撃者がサーバー上でコマンドを実行できないわけではありません。アプリケーションがデータベースに使用するアカウントの権限レベルが相応に高ければ、攻撃者は、次のコマンドを挿入するだけで、このオプションを再び有効にすることができます。

EXECUTE SP_CONFIGURE 'xp_cmdshell', '1'

SQL インジェクションから保護する方法: まず、この問題を解決できなかった方法から見てみましょう。従来の ASP アプリケーションで非常によく使用されていた解決策は、単純にダッシュや引用符を別の文字に置き換えてしまう方法でした。残念なことに、この解決策は唯一の保護手段として .NET アプリケーションでいまだに広く使用されています。

string safeSql = "select * from users where loginId = " + userInput.Replace("—-", "");

safeSql = safeSql.Replace("'","''");

safeSql = safeSql.Replace("%","");

このアプローチは、次のことを想定しています。

  1. 「このような呼び出しにより、すべてのクエリがすべて正しく保護される」。これは、既定で保護が行われるパターンを使用する代わりに、このようなインライン チェックをすべての箇所に含めるのを開発者が忘れないことを前提にしています。開発者が週末をすべてコーディングに費やし、カフェインを切らした状態であったとしてもです。
  2. 「すべてのパラメーターで型チェックが行われる」。Web ページのパラメーターを、そのパラメーターは数値であるはずだという理由だけで、実際に数値かどうかを確認せず、文字列の確認をまったく行わずに ProductId などとしてクエリで使用することは、開発者にありがちなケースです。攻撃者がこの ProductId を変更し、次のようにクエリ文字列から単純に読み取られるとどうなるでしょう。
URI: http://yoursite/product.aspx?productId=10

これによって、以下のステートメントが生成されます。

select * from products where productid=10

さらに、攻撃者が次のようなコマンドを挿入すると、

URI: http://yoursite/product.aspx?productId=10;select 1 col1 into #temp; drop table #temp;

以下のクエリになります。

select * from products where productid=10;select 1 col1 into #temp; drop table #temp;

困ったことに、文字列関数または型チェックによってフィルターがかけられずに整数フィールドに挿入されてしまいます。これを直接インジェクション攻撃と呼びます。これは、引用符を必要とせず、挿入部分が引用符なしでクエリで直接使用されるためです。「常にすべてのデータをチェックするようにしている」と言われるかもしれませんが、それは、手動ですべてのパラメーターを確認する役割を開発者に負わせていることになり、ミスが発生する確率が高くなります。代わりに、アプリケーション全体でより適切なパターンを使用して、正しい方法で解決する必要があります。

それでは、SQL インジェクションを適切に防ぐにはどうすればよいでしょう。これは、ほとんどのデータ アクセス シナリオで非常に簡単に実現できます。鍵は、パラメーター化呼び出しを使用することです。呼び出しをパラメーター化する限り、動的 SQL が実際に安全に保たれます。次に、基本的な規則を示します。

  1. 以下のものだけを使用するようにします。
    • (動的 SQL を使用しない) ストアド プロシージャ
    • パラメーター化クエリ (図 2 参照)

図 2 パラメーター化クエリ

using (SqlConnection connection = new SqlConnection(  ConfigurationManager.ConnectionStrings[1].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note we use a dynamic 'like' clause

    string query = @"Select Name, Description, Keywords From Product

                   Where Name Like '%' + @ProductName + '%'

                   Order By Name Asc";

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      // Get data

      DataSet dataSet = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(dataSet, "ProductResults");

      // Populate the datagrid

      productResults.DataSource = dataSet.Tables[0];

      productResults.DataBind();

    }

  }

}
  • ストアド プロシージャのパラメーター化呼び出し (図 3 参照)

図 3 ストアド プロシージャのパラメーター化呼び出し

//Example Parameterized Stored Procedure Call

string searchText = txtSearch.Text.Trim();

using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note: you do NOT use a query like: 

    // string query = "dbo.Proc_SearchProduct" + productName + ")";

    // Keep this parameterized and use CommandType.StoredProcedure!!

    string query = "dbo.Proc_SearchProduct";

    Trace.Write(string.Format("Query is: {0}", query));

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      command.CommandType = CommandType.StoredProcedure;

      // Get the data.

      DataSet products = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(products, "ProductResults");

      // Populate the datagrid.

      productResults.DataSource = products.Tables[0];

      productResults.DataBind();

    }

  }

}
  1.  2.   ストアド プロシージャ内の動的 SQL を、sp_executesql へのパラメーター化呼び出しにします。また、パラメーター化呼び出しをサポートしないため、exec は使用しません。さらに、ユーザー入力と文字列とを連結しないようにします。図 4 を参照してください。

図 4 sp_executesql へのパラメーター化呼び出し

/*

This is a demo of using dynamic sql, but using a safe parameterized query

*/

DECLARE @name varchar(20)

DECLARE @sql nvarchar(500)

DECLARE @parameter nvarchar(500)

/* Build the SQL string one time.*/

SET @sql= N'SELECT * FROM Customer  WHERE FirstName Like @Name Or LastName Like @Name +''%''';

SET @parameter= N'@Name varchar(20)';

/* Execute the string with the first parameter value. */

SET @name = 'm%'; --ex. mary, m%, etc. note: -- does nothing as we would hope!

EXECUTE sp_executesql @sql, @parameter,

                      @Name = @name;

exec はパラメーターをサポートしないため、"exec 'select .. ' + @sql" のような使い方はしないように注意してください。

  1.  3.   ダッシュや引用符を別の文字に置き換えるだけで安全になるわけではありません。以前に説明した、開発者による手動の操作が要らない、SQL インジェクションを防ぐ一貫したデータ アクセス メソッドを使用し、そのアプローチに固執しましょう。拡張ルーチンを用意しても、1 か所でも呼び出し忘れると、攻撃に合う可能性があります。さらに、拡張ルーチンを実装する方法には、SQL 切り捨て攻撃の場合のような脆弱性があります。
  2.  4.   型チェックおよびキャストを使用して、入力を検証します (次の「パラメーターの改ざん」を参照してください)。また、英数字データなどに制限するために正規表現を使用するか、重要なデータは既知のソースから取得するようにします。さらに、Web ページのデータを信頼しないようにします。
  3.  5.   データベース オブジェクトの権限を監査して、アプリケーション ユーザー スコープを制限し、攻撃の対象を狭めます。更新、削除、挿入などの権限は、ユーザーがこうした操作を必要とする場合のみに与えます。アプリケーションにはそれぞれ、データベースへの固有のログインを、権限を制限した状態で与えます。私が開発したオープン ソースのソフトウェア、SQL Server Permissions Auditor がこれに役立ちます。sqlpermissionsaudit.codeplex.com (英語) を参照してください。

パラメーター化クエリを使用する場合、テーブルのアクセス権を監査することが非常に重要です。テーブルにアクセスする権限がユーザーまたはロールに必要になります。1 つのアプリケーションを SQL インジェクションから保護しても、保護されていない別のアプリケーションがデータベースにアクセスするとどうなるでしょう。攻撃者は、データベースにクエリできてしまいます。そのため、どのアプリケーションにも、独自の制限されたログインを用意します。また、ビュー、プロシージャ、テーブルなどのデータベース オブジェクトのアクセス権も監査します。ストアド プロシージャを使用する場合、一般にストアド プロシージャ内で動的 SQL を使用していない限り、テーブルではなくプロシージャ自体にアクセス許可があればよいので、セキュリティの管理はやや簡単になります。これにも、SQL Server Permissions Auditor が役立ちます。

Entity Framework は、暗黙のうちにパラメーター化クエリを使用しているため、普通の使用シナリオでは、SQL インジェクションから保護されます。動的パラメーター化クエリのためにテーブルのアクセス許可を与えるのではなく、ストアド プロシージャにエンティティをマップするのを好む人もいますが、両方に有効な引数があるので、どちらを選んでもかまいません。Entity SQL を明示的に使用している場合は、クエリのセキュリティに関する追加の考慮事項について把握しておいてください。詳細については、MSDN ライブラリの「セキュリティに関する注意事項 (Entity Framework)」(msdn.microsoft.com/library/cc716760) を参照してください。

パラメーターの改ざん

概要: パラメーターの改ざんとは、アプリケーションが想定している機能を変えるためにパラメーターを変更する攻撃です。パラメーターは、フォーム、クエリ文字列、Cookie、データベースなどで使われます。ここでは、Web ベースのパラメーターに関係する攻撃について説明します。

悪用方法: 攻撃者は、アプリケーションに想定外の操作を実行させるために、パラメーターを変更します。たとえば、クエリ文字列からユーザー ID を読み取ってユーザーのレコードを保存するのは、安全ではありません。攻撃者は、図 5 のような方法を用いて、アプリケーションの URL を改ざんすることができます。

変更された URL
図 5 変更された URL

こうすることで、攻撃者は、ユーザー アカウントを読み取ることが可能になります。これは、想定外の操作です。また、たいていの場合、次のようなアプリケーション コードは、userID を盲目的に信頼してしることになります。

// Bad practice!

string userId = Request.QueryString["userId"];

// Load user based on this ID

var user = LoadUser(userId);

適切な方法: フォームを信頼するのではなく、ユーザー セッションなど、より信頼性の高いソースや、メンバーシップ/プロファイル プロバイダーから値を読み取ります。

多くのツールによって、クエリ文字列でなくても実に簡単に改ざんされます。Web ブラウザー開発者のツール バーをいくつか確認して、ページ上の非表示要素を表示できるかどうかを確かめてみてください。何がわかってしまうか、データをいかに簡単に改ざんできるかを知って、驚かれると思います。図 6 の [Edit User] (ユーザーの編集) ページについて考えてみましょう。ページ上の非表示フィールドを表示できれば、フォームに埋め込まれているユーザー ID を確認できるため、改ざんが可能になります (図 7 参照)。このフィールドは、このユーザー レコードの主キーとして使用されているため、改ざんすると、データベースに保存されるレコードが変更されます。

[Edit User] (ユーザーの編集) フォーム
図 6 [Edit User] (ユーザーの編集) フォーム

非表示フィールドが表示されたフォーム
図 7 非表示フィールドが表示されたフォーム

パラメーター改ざんの保護方法: ユーザーが入力したデータを信頼せず、データの重要性に基づいて受け取るデータを検証します。基本的に、プロフィールに保存されているミドル ネームなど、それほど重要ではない値をユーザーが変更しても、気に留めません。ですが、ユーザー レコードのキーとなる、非表示のフォーム ID が改ざんされたかどうかは、間違いなく問題にします。このような場合、Web ページではなく、サーバーの既知のソースから信頼できるデータを取得します。このような情報は、ログイン時のユーザー セッション、またはメンバーシップ プロバイダーに保存されています。

たとえば、優れたアプローチでは、以下のように、フォーム データを使用するのではなく、メンバーシップ プロバイダーの情報を使用します。

// Better practice

int userId = Membership.GetUser().ProviderUserKey.ToString();

// Load user based on this ID

var user = LoadUser(userId);

ブラウザーから取得するデータがいかに信頼できないかわかったところで、内容を整理するため、このようなデータ検証の例をいくつか見てみましょう。以下に、典型的な Web フォームのシナリオを示します。

// 1. No check! Especially a problem because this productId is really numeric.

string productId = Request.QueryString["ProductId"];

// 2. Better check

int productId = int.Parse(Request.QueryString["ProductId"]);

// 3.Even better check

int productId = int.Parse(Request.QueryString["ProductId"]);

if (!IsValidProductId(productId))

{

    throw new InvalidProductIdException(productId);

}

図 8 では、明示的にパラメーターをキャストしなくても基本的な型変換を自動で実行する、モデル バインディングによる典型的な MVC シナリオを示しています。

図 8 MVC のモデル バインディングの使用

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult Edit([Bind(Exclude="UserId")] Order order)

{

   ...

   // All properties on the order object have been automatically populated and 

   // typecast by the MVC model binder from the form to the model.

   Trace.Write(order.AddressId);

   Trace.Write(order.TotalAmount);

   // We don’t want to trust the customer ID from a page

   // in case it’s tampered with.

   // Get it from the profile provider or membership object

   order.UserId = Profile.UserId;

   // Or grab it from this location

   order.UserId = Membership.GetUser().ProviderUserKey.ToString();

   ...

   order.Save();}

   ...

   // etc.

}

モデル バインディングは、モデル - ビュー - コントローラー (MVC: Model-View-Controller) の優れた機能で、パラメーター チェックに役立ちます。Order オブジェクトのプロパティが、フォーム情報に基づいて、自動的に設定され、定義済みの型に変換されます。モデルでデータ注釈を定義して、さまざまな検証を含めることも可能です。ただし、設定できるプロパティを制限する必要があります。さらに、繰り返しになりますが、重要な項目についてはページのデータを信頼しないようにしてください。優れた経験則としては、各ビューに 1 つのビューモデルを含めます。この Edit の例であれば、モデルから UserId を完全に除外します。

ここでは、実行したいものと信頼しないものを管理するために、[Bind(Exclude)] 属性を使用して MVC が Model にバインドするものを制限している点に注目してください。これにより、UserId が、フォーム データから取得されなくなり、改ざんが不可能になります。モデル バインディングとデータの注釈は、この記事では扱いません。ここでは、Web フォームと MVC の両方でパラメーターの型変換が機能するしくみを簡単に確認しました。

ID フィールドを "信頼する" Web ページに含める必要がある場合は、MVC Security Extensions (mvcsecurity.codeplex.com) に、便利な属性があります。

まとめ

今回は、アプリケーションのハッキングによく使用される方法を 2 つ紹介しました。もうおわかりのように、アプリケーションにほんの少し変更を加えるだけで、攻撃を防いだり、少なくとも制限したりすることが可能です。もちろん、これらの攻撃のバリエーションや、その他の方法によって、アプリケーションがハッキングされる場合があることにも留意してください。次回は、別の種類の攻撃である、クロスサイト スクリプトと、クロスサイト リクエスト フォージェリについて扱います。

Adam Tuliper は、Cegedim 社のソフトウェア アーキテクトで、20 年以上にわたってソフトウェア開発に取り組んでいます。彼は、国内の INETA コミュニティの講演者です。また、会議や .NET ユーザー グループで定期的に講演しています。彼の Twitter は twitter.com/AdamTuliper (英語) から、ブログは completedevelopment.blogspot.com (英語) または secure-coding.com (英語) からご覧いただけます。

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