はじめに
私は悪意のあるプログラムの作り方を知っています。随分昔になりますが、古いcommand.comにMichelangeloウイルスを埋め込む方法を見つけたときには、command.comでスピーカーのポートを開いたり、ビープ音が鳴り続けるようにしたことがあります。BIOS割り込みでキーボードの入力を監視するプログラムを書いたこともあります。これは20年ほど前の話で、それ以降、その好奇心を育てることはありませんでした。ですから、時々友人がボットやワーム、ポップアップ広告などを駆除するのを手伝ったりしていることを除けば、決して悪意あるコードのエキスパートなどではありません。むしろ、その手のコードを作成する輩にはうんざりしています。
そうは言っても、悪意のあるコードを書く人々はやはり存在します。おそらく彼らの行動の裏には、好奇心が強いとか、雇い主やだれかから何かをくすねようとしているなどの理由があるのでしょう。このような悪意の現れの1つに、SQLインジェクション攻撃があります。
SQLインジェクションの流れはこうです。何らかのユーザー入力から値を受け取るSQLがあり、そのSQLがデータベースに渡されるとします。悪意あるユーザーはこのSQLに、所定のパラメータの代わりに、自分で書いたSQLを最後に付け加えたパラメータを渡して、不正な処理を実行させようとします。
本稿では、LINQ to SQLを使って空のクエリに値を設定することでSQLインジェクションを防止する方法について解説します。つまり、LINQはデータベースにSQLを渡すための優れた代替手段です。LINQ to SQLは通常のADO.NETやSQLと比べて簡単なばかりでなく、SQLインジェクションの防止という設計上の副次効果が期待できます。
悪意あるSQLインジェクションとはどういうものか
Webサイトの設計によっては、SQLコマンドの組み立てにユーザー入力データを用いているかどうかを、ごく簡単に見分けることができます。ブラウザでWebサイトを開き、SQLの未終了文字列のエラーを発生させるようなデータ(一重引用符など)を入力し、そのWebサイトが暴走するかどうかを確認するのです。Webサイトが暴走したとき、そのサイトが運よくデバッグモードで配置されていれば、おそらくADO.NETのコードが実際に表示されます。
図1は、顧客IDを入力すると対応する会社名が表示される簡単なWebサイトです(ほとんどの読者がアクセスできるように、Northwindデータベースを使用)。ただし、悪意あるユーザーが一重引用符を入力すると、このサイトはクラッシュします。デバッグモードの場合、入力したコードをSQL Serverに送っているという情報が、悪意あるユーザーに知られてしまいます。
こうなると、悪意あるユーザーは顧客IDの代わりに巧妙なSQLを組み立てて、フィッシングに取りかかります。最も狙われやすい場所が、マスタテーブルです。まずリスト1で、このサンプルWebサイトのコードを見てください。
Imports System.Data.SqlClient Partial Public Class _Default Inherits System.Web.UI.Page Protected Sub Page_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load End Sub Protected Sub Button1_Click(ByVal sender As Object, _ ByVal e As EventArgs) Handles Button1.Click Dim connectionString As String = _ "Data Source=.\SQLExpress;Initial>span _ Catalog=northwind;Integrated Security=True" Dim sql As String = _ "SELECT CustomerID, CompanyName FROM Customers _ WHERE CustomerID = '{0}'" Using connection As SqlConnection = _ New SqlConnection(connectionString) connection.Open() Dim command As SqlCommand = _ New SqlCommand(String.Format(sql, _ TextBox1.Text), connection) Dim table As DataTable = New DataTable Dim adapter As SqlDataAdapter = New SqlDataAdapter(command) adapter.Fill(table) GridView1.DataSource = table GridView1.DataBind() End Using End Sub End Class
このサンプルでは、コードのとおりに「TextBox.Text」から値を読み込み、エラーチェックをしないでその値をSQLに埋め込みます(このようなコーディングはお勧めしません。こうしたコードが本番環境で使われている場合もありますが、上記はインジェクションの仕組みを示すごく典型的な例です)。
次に、悪意あるユーザーが、顧客IDを入力せずに次のようなテキストを入力したとします。
"ALFKI' UNION SELECT 'database name' as dummy, name _ FROM master.sys.databases --"
上記のテキストによって、一見すると何の変哲もないリスト1のSQLから、次のSQLができあがります。
SELECT CustomerID, CompanyName FROM Customers _ WHERE CustomerID = 'ALFKI' UNION _ SELECT 'database name' as dummy, name from master.sys.databases
インジェクションの最後に付けられたコメント文字(--)により、元のSQLの一重引用符が削除され、新たな一重引用符とUNION SELECT文に置き換えられます。このUNION SELECT文が成功すると、サーバ上にあるすべてのデータベースのユーザーが表示されてしまいます。このとき、Webページには図3のような結果が表示されるでしょう。
このような簡単な例であれば、修正する方法はいくつもあります。ドロップダウンだけを表示することもできますし、入力値を正規表現でフィルタリングして、DELETE FROM文、一重引用符およびSQLのコメント(--)など、害を及ぼす危険性のある不適当な値が入力されていないかを調べることもできます。また、LINQ to SQLを使用するという手もあります。