japan.internet.com
japan.internet.com メンバーID
Twitter
Facebook
RSS
ピックアップ
2008年10月31日 10:00

LINQ to SQLが通常のSQLより優れた選択肢である理由

著者Paul Kimmelオリジナル版を読む海外海外発

はじめに

 私は悪意のあるプログラムの作り方を知っています。随分昔になりますが、古い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に送っているという情報が、悪意あるユーザーに知られてしまいます。

図1 正しい顧客IDを入力すると会社名が表示される
図1 正しい顧客IDを入力すると会社名が表示される
図2 閉じていない引用符のエラーが発生してソース行が表示される。それほどエラーチェックをしないでユーザー入力をSQL Serverに渡していることが分かる
図2 閉じていない引用符のエラーが発生してソース行が表示される。それほどエラーチェックをしないでユーザー入力をSQL Serverに渡していることが分かる
 こうなると、悪意あるユーザーは顧客IDの代わりに巧妙なSQLを組み立てて、フィッシングに取りかかります。最も狙われやすい場所が、マスタテーブルです。まずリスト1で、このサンプルWebサイトのコードを見てください。

リスト1 SQLインジェクション攻撃を受けやすいコード。ユーザー入力がそのままSQL Serverに送信されている
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のような結果が表示されるでしょう。

図3 UNION SELECT文が成功すると、無害なWebページは悪意あるユーザーのスパイツールや破壊ツールに変わり果てる
図3 UNION SELECT文が成功すると、無害なWebページは悪意あるユーザーのスパイツールや破壊ツールに変わり果てる
 このような簡単な例であれば、修正する方法はいくつもあります。ドロップダウンだけを表示することもできますし、入力値を正規表現でフィルタリングして、DELETE FROM文、一重引用符およびSQLのコメント(--)など、害を及ぼす危険性のある不適当な値が入力されていないかを調べることもできます。また、LINQ to SQLを使用するという手もあります。

LINQ to SQLによるSQLインジェクションの排除

 論点をシンプルにするため、サンプルWebアプリケーションをコンソールアプリケーションに書き換えたものがリスト2です。このコンソールアプリケーションは、SQL文に埋め込まれる悪意あるSQLを単純化して表現しています(話を簡単にするために、どうやって埋め込むかは重要視しません。前のセクションで、値が直接埋め込まれるSQLにはインジェクションのおそれがあることを説明したので、不備のあるSQLが最終的に好ましくない結果となることが分かれば十分です)。

リスト2 検討のために簡略化したSQLインジェクション
Imports System.Data.SqlClient

Module Module1

   Sub Main()
      SqlInjectionToDiscoverOtherDatabase()
   End Sub

   Sub SqlInjectionToDiscoverOtherDatabase()
      Dim connectionString As String = _
         "Data Source=.\SQLExpress;Initial _
            Catalog=northwind;Integrated Security=True"

      Dim sql As String = _
         "SELECT CustomerID, CompanyName FROM Customers _
            WHERE CustomerID = '{0}'" _
      Dim customerID = _
         "ALFKI' UNION SELECT 'database name' as dummy, " + _
         "name FROM master.sys.databases --"

      Using connection As SqlConnection = _
         New SqlConnection(connectionString)
         connection.Open()
      Dim command As SqlCommand = _
         New SqlCommand(String.Format(sql, customerID), connection)
      Dim reader As SqlDataReader = command.ExecuteReader

      While (reader.Read())
         Dim i As Integer
         For i = 0 To reader.VisibleFieldCount - 1
            Console.WriteLine(reader(i).ToString())
         Next
      End While

      End Using

      Console.ReadLine()

   End Sub

End Module
 リスト2では、悪意のあるSQL(ユーザーによる入力)を表すcustomerIDローカル変数を宣言しています。LINQ to SQLによってSQLインジェクションがどのように排除されるかを解説する前に、ここでLINQ to SQLについての一般的な説明をしたいと思います。LINQ to SQLの基本がわかっている場合は、次のセクションを読み飛ばしてください。

LINQ to SQLとは

 LINQ(Language Integrated Query)とは、VBまたはC#のコードで記述されたSQLライクな言語のことです。LINQ to SQLは、基本的にLINQ to SQLテクノロジによってSQLクエリに変換されるLINQコードを指します。

 大まかに言って、LINQ to SQLの作成手順は次のようになります。SQLデータベースのテーブルに一致する、カスタムエンティティクラスを定義します。クラスにはTableAttribute属性を指定し、列を表すプロパティにはColumnAttribute属性を指定します。SqlMetalを使用して、これらのクラスを生成することもできます。これはまさに、定義した(またはSqlMetal.exeで生成した)カスタムクラスのプロパティと、ColumnAttribute属性でテーブルの列に関連付けられているプロパティのみがSQLから返されることを意味しています。次に、DataContextクラスを継承した簡単なクラスを定義します。このDataContextクラスは、データベースへの接続を表しています。作成はこれで完了です。ADO.NETを直接使用することはありません。SQLを直接記述することもありません。周辺の処理はLINQ to SQLの仕事です。

 最終的な結果として、エンティティに一致するデータのみが返されます。ユーザーは、動的SQL文字列ではなくコンパイル済みのコードを使用することになります。SQLに直接渡す値はなく、SQLはLINQ to SQLエンジンによって生成されます。

LINQ to SQLがSQLインジェクションを防止する仕組み

 動的SQLでは、SQLエンジンにテキスト文字列が渡されることが問題の種となります。LINQ to SQLの場合、コードは既にコンパイルされていて、SQLはLINQ to SQLテクノロジによって生成されます。

 次に重要なのは、LINQがサポートするのはSelectクエリのみであるという点です。Update、Delete、Insertはサポートしていません。このため、悪意あるユーザーにできるのはデータを読み込むことだけです。これは少々不便に思われるかもしれませんが、そんなことはありません(詳しくは後述)。

 LINQは、コンパイル時にメソッド呼び出しに変換されます。つまり、実行コードはコンパイルが完了しています。そのため、ユーザーが実行時に新たなLINQキーワードを入力して、事前に定義されたクエリを改ざんすることはできません。実行時にSelectをJoinやUnionに変えることはできないのです。

注意
 CodeDOMを使用して、LINQクエリを実行時にコンパイルするようにアプリケーションを設計すると、実行時にユーザーが動的LINQを入力できる場合があります。ただし、当然ながらこれには高度な技術を要します。方法が分かる人はあまりいませんし、ビジネスアプリケーションで動的コンパイルを使用する理由はほとんどありません。
 ここまでをまとめてみると、LINQ to SQLでインジェクションが排除されるのは、ユーザーが動的クエリではなくコンパイル済みのコードを使用するためです。LINQクエリが実行するのは読み取りだけなので、Selectは可能ですが、Delete、UpdateまたはInsertは不可能です。バックエンドにあるデータの変更は、クエリではなくメソッド呼び出しを経由して行われます。リスト3では、リスト2のコード(コンソールアプリケーション)を改造して、LINQ to SQLで同じタスクを実行する方法を解説します。

リスト3 結果セットをフィルタするための値をユーザー(ローカル変数で表現)に入力させるコード
Imports System.Linq
Imports System.Data.Linq
Imports System.Data.Linq.Mapping

Module Module1

   Sub Main()
      CorrectedVersionWithLINQToSQL()
   End Sub
   Sub CorrectedVersionWithLINQToSQL()

      Dim northwind As Northwind = New Northwind
      Dim customers As Table(Of Customer) = _
         northwind.GetTable(Of Customer)()

      Dim param As String = "ALFKI"

      Dim results = From cust In customers _
                    Where cust.CustomerID = param _
                    Select cust

      For Each c In results
         Console.WriteLine(c.CustomerID)
         Console.WriteLine(c.CompanyName)
      Next

      Console.ReadLine()

   End Sub

End Module

Public Class Northwind
   Inherits DataContext

   Public Shared ReadOnly connectionString AsString = _
      "Data Source=.\SQLExpress;Initial _
         Catalog=northwind;Integrated Security=True"

   Public Sub New()
      MyBase.New(connectionString)
   End Sub
End Class

<Table(Name:="Customers")> _
Public Class Customer

   Private FCustomerID As String
   <Column()> _
   Public Property CustomerID() As String
      Get
         Return FCustomerID
      End Get
      Set(ByVal Value As String)
         FCustomerID = Value
      End Set
   End Property

   Private FCompanyName As String
   <Column()> _
   Public Property CompanyName() As String
      Get
         Return FCompanyName
      End Get
      Set(ByVal Value As String)
         FCompanyName = Value
      End Set
   End Property

End Class
 Northwindクラスには、バックエンドのSQLデータベースへの接続に必要なものすべてが揃っています。ADO.NETを直接使用することはありません。NorthwindクラスはDataContextクラスを継承しており、接続文字列を指定してベースのコンストラクタを呼び出します。Customerクラスはエンティティクラスと呼ばれるクラスです。このクラスは、TableAttribute属性でCustomersテーブルにマッピングされ、設計したとおりにCustomersテーブルからCustomerIDとCompanyNameのみを返します。DataContextクラスとTableクラスが他の場所からこれ以外のデータを返すことはできません。

 LINQ to SQLクエリは、Moduleに記述されています。Northwindクラスのインスタンスを生成してTable(Of Customers)にリクエストを送信し、LINQクエリで目的のデータを選択する単純なものです。このサンプルを見れば分かるように、LINQはコンテキストを設定するFrom句で始まり、Select句で終わります。Intellisense(インテリセンス)による補助もあります(初めは少し戸惑いますが、すぐに慣れるでしょう)。

免責事項
 混乱を引き起こそうと画策する悪知恵の働く者が大勢いるようなので、たった1つの方法であらゆる攻撃を阻止できる保証はありません。攻撃を受けて損害を被る可能性があるときは、情報の保護に必要な時間、労力およびコストと比較して検討する必要があります。また、セキュリティというのは全体論的な問題であり、企業内外の人々とその行動をひっくるめて考える必要があります。
 読者の皆さんは非常に優秀な方ばかりだと思うので、興味があれば、悪意のある値をパラメータに挿入して有用な情報を入手する方法を考えてみてください。条件として、変更できるのはパラメータの値のみとします。成功した場合には、ぜひその成果を発表してください。

まとめ

 LINQ to SQLでは、ユーザーに動的SQLではなくコンパイル済みのコードを使用させることで、SQLインジェクションを回避します。これはつまり、エンドユーザーは本来の意図とは異なるクエリをパラメータとして渡すことができないということです。このクエリ言語はSelect構文のみをサポートし、Update、DeleteまたはInsertはサポートしていないため、LINQ to SQLでは有害なSQLインジェクションが阻止されます。更新処理はすべてメソッド呼び出しを経由して行われるため、当然ながらインジェクションは不可能です(インジェクションが機能しうる唯一の方法は、CodeDOMを使用して、ユーザーの入力した文字列を動的にコンパイルするコードを書く場合です)。

 最後になりますが、LINQ to SQLで私が最も気にいっているのは、ADO.NETで周辺処理をすべて記述することに比べて、LINQ to SQLの方が書きやすく使いやすい点です。LINQを本気でマスターしたいときは、Amazon.co.jpやどこか良い書店で拙著『LINQ Unleashed for C#』をお買い求めください。VBしかわからないという方でも、解説は普通の文章(英語)で書かれていますし、コードはダウンロード可能なので、ぜひお試しください。VBとC#は二卵性の双子のようなものなので、VBが分かればC#も読むことができます。

著者紹介

Paul Kimmel(Paul Kimmel)
www.codeguru.comでVisual Basic Todayの執筆を担当。オブジェクト指向プログラミングや.NETについての著作が数冊ある。最新刊は『UML DeMystified』(McGraw-Hill/Osborne刊)。Tri-State Hospital Supply Corporationのアーキテクトを務める。テクノロジに関する疑問や質問はpkimmel@softconcepts.comまで。
.NETユーザーグループへの参加または支援について関心がある方はwww.glugnet.orgをご覧ください。

プリンター用
記事を転送
この記事をクリップ!
【特別連載企画】大艦巨砲主義にして卓越したレスポンス--GALAXY S II WiMAX
【特別連載企画】大艦巨砲主義にして卓越したレスポンス--GALAXY S II WiMAX 1月20日より販売が開始されたサムスン製スマートフォン「GALAXY S II WiMAX」。カタログスペックでは、他メーカーのハイエンド機と同じように見えても、実際に使うと卓越したレスポンスに驚かされる。
⇒詳細記事はこちら
⇒連載記事一覧はこちら
注目のトピックス
最新コラム一覧
百式のネットビジネス研究
百式のネットビジネス研究
フリーランスな人が多い今だからこそ…「FREELANCE THANKS」
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
12月の主婦層、ベルメゾンが首位を維持(VRI 調査)
アウンのグローバルマーケティング動向
アウンのグローバルマーケティング動向
Web プロモーションにおいて大切なこと―年度末編―
多言語×Web×海外マーケティング情報
多言語×Web×海外マーケティング情報
海外発、注目 AR プロモーション
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
楽天が目指す変革──Globalization、Agile、Big Data
中国・台湾ネットビジネス情報最前線
中国・台湾ネットビジネス情報最前線
中国から Web を見てもらいたいならば
マーケティングに活用できる最新トレンド
マーケティングに活用できる最新トレンド
改めて、「導線」最適化に目を向ける
次世代マーケティングチェーンの視点
次世代マーケティングチェーンの視点
ソーシャル時代における BtoC 型 Eコマース成功のポイント
Copyright 2012 internet.com K.K. (Japan) All Rights Reserved.