プライバシーポリシー

2008年12月22日月曜日

JavaとMySQLの組み合わせでUnicodeのU+00A5を用いたSQLインジェクションの可能性

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2)。
備忘のため転載いたしますが、この記事は2008年12月22日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

今年のBlack Hat Japanには、はせがわようすけ氏が「趣味と実益の文字コード攻撃」と題して講演され話題となった。その講演資料が公開されているので、私は講演は聞き逃したが、資料は興味深く拝見した。その講演資料のP20以降には、「多対一の変換」と題して、UnicodeのU+00A5(通貨記号としての¥)が、他の文字コードに変換される際にバックスラッシュ「\」(日本語環境では通貨記号)の0x5Cに変換されることから、パストラバーサルが発生する例が紹介されている。

しかし、バックスラッシュと言えばSQLインジェクションの可能性も見逃すことができない。そこで、本資料をきっかけとして、U+00A5を使ったSQLインジェクションの可能性について調査し、Java(JDBC)とMySQLの組み合わせにおいて、発生する場合があることを確認したので報告する。

U+00A5を用いたSQLインジェクションとは

ここで、U+00A5を用いたSQLインジェクションとはどのようなものかを説明しよう。UnicodeのU+00A5はバックスラッシュとは独立に扱える日本円の通貨記号として割り当てられている。この文字をShift_JISやEUC-JPなどに変換する際に、ASCIIの0x5Cに変換される(場合がある)。すると、バックスラッシュをSQLのエスケープに使用するデータベース、具体的にはMySQLとPostgreSQLにおいて、SQLインジェクションが発生する場合がある
具体例を用いて説明しよう。検査パターンとして以下の文字列を使用する。以下、U+00A5を表記する場合には赤色全角の通貨記号「」を用いる
'OR 1=1#
先頭の文字がU+00A5である。これをMySQLのルールでエスケープすると、シングルクォートが「\'」と変換され、以下のようになる。
\'OR 1=1#
ややこしいが、最初の通貨記号がU+00A5、二番目の通貨記号が0x5Cである。これをShift_JISあるいはEUC-JPに変換すると以下のように、二文字とも0x5Cになる。
\\'OR 1=1#
これをSQLとして解釈すると、最初の「\\」が「\」をエスケープしたものと見なされ、「'」はエスケープされない状態となる。すなわち、SQLインジェクションされたことになる。

どのような場合に問題になるか

このタイプのSQLインジェクションが発生するのは、以下のようなケースが典型的な場合であろう。

外部とのインターフェースにUnicode(典型的にはUTF-8)を用いていて、U+00A5を入力することができる
アプリケーションの内部でもUnicode(UCS-2、UTF-16、UTF-8など)を用いている
SQLのエスケープはUnicodeの状態で実行している
アプリケーションからデータベースのクエリ実行までのどこかで、Unicode以外の文字コード(典型的には、Shift_JISかEUC-JP)に変換されている
内部コードとしてUnicodeを用いる言語は現在では数多いが、筆者はJavaとPerl(use utf8;)を用いて検証した。その結果、JavaとMySQLの組み合わせの場合にSQLインジェクションが発生する場合があることを確認した

検証コードの説明

以下のような検証コードを用いてテストした。
import java.sql.*;
public class MyA5Injection {
  public static void main(String[] args) {
    try {
      String charEncoding = "sjis";    // or "utf8"
      Class.forName("com.mysql.jdbc.Driver");
      Connection con = DriverManager.getConnection(
        "jdbc:mysql://localhost/tokumaru?user=xxx&password=xxxx&useUnicode=true&characterEncoding=" + charEncoding);
      Statement stmt = con.createStatement();

      String param = "\u00a5'or 1=1#";

      // MySQL用のエスケープ
      String e_param = param.replaceAll("\\\\", "\\\\\\\\");    // \ → \\
      e_param = e_param.replaceAll("'", "\\\\'");               // ' → \'

      String sql = "SELECT * FROM test WHERE name='" + e_param + "'";
      System.out.println("sql = " + sql);
      ResultSet rs = stmt.executeQuery(sql);
      while(rs.next()){
        int id = rs.getInt("id");
        String name = rs.getString("name");
        System.err.println(id + " " + name);
      }
      stmt.close();
      con.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
}

実行結果は以下の通り
C:\HOME\Java>java MyA5Injection
sql = SELECT * FROM test WHERE name='\\'or 1=1#'
~ 検索結果の表示 ~

テスト結果

U+00A5を用いたSQLインジェクションは、JDBCのgetConnectionメソッドに指定するオプションパラメータcharacterEncodingに依存するようだ。このパラメタがUTF-8の場合はSQLインジェクションは発生しない。一方、Shift_JISやEUC-JPの場合はSQLインジェクションが発生する。create tableのdefault charset設定には依存しないようだ。これらを下表にまとめた。


UTF-8のテーブル Shift_JISのテーブル
characterEncoding=utf8 正常処理 エラー(*1)
characterEncoding=sjis SQLインジェクション SQLインジェクション
検証に用いた環境
MySQL 5.0 および 5.1
MySQL Connector/J 5.1.7
JDK6 Update11
Windows XP Professional 

(*1) java.sql.SQLException: Illegal mix of collations (sjis_japanese_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='

現実的に脆弱となる組み合わせはどの程度使用されているか

現実にSQLインジェクションが発生するは、JavaとMySQLの組み合わせすべではなく、characterEncodingの指定が明示的あるいは暗黙にutf8以外の値になっている場合と考えられる。筆者が試した範囲では、MySQLのコンフィグレーション・ウィザードで「Best Support for Multilingualism MySQL」を指定した場合にはUTF-8が利用されるが、それ以外の場合はlatin1、あるいはユーザが指定した文字エンコーディング(Shift_JISなど)が設定される。また、GoogleでgetConnectionを検索すると、characterEncoding=sjisと記述した例が多数ヒットしている。そのような状況では、characterEncodingとしてUTF-8以外が指定されている比率は割合に多いのではないかと予想する。

その他の言語とDBの組み合わせの場合はどうか

筆者が他の組み合わせで試した範囲では、Java+PostgreSQLやPerl+MySQLではSQLインジェクションにはならなかった。Java+PostgreSQLの場合はエラーになり、Perlの場合はU+00A5が「?」に変換されるようで、やはりSQLインジェクションにはならなかった。しかし、筆者が試したものと別の条件ではSQLインジェクションが発生する可能性はゼロではない。

対策

はせがわようすけ氏の講演資料には以下のような対策が推奨されている
  • Unicodeのまま文字列を扱い、変換しない
  • (変換するとしても)検査後には変換しない
SQLインジェクション対策としても「変換しない」というガイドラインは有効である。すなわち、以下を推奨する。
  • characterEncoding=utf8を明示する(必須)
  • create tableの際のdefault charsetにもutf8を設定する(推奨)

追記(2008/12/22 14:00)

金床氏から「例のコードがPreparedStatementじゃないのは何故だろう」という指摘を受けた。原理を示すためにはエスケープの方が分かりやすいと思ったからだが、PreparedStatementでも試してみた。主なコードの変更点は以下の通り(エスケープ処理は必要なくなる)。
String sql = "SELECT * FROM test where name=?";
PreparedStatement stmt = con.prepareStatement(sql);
stmt.setString(1, param);
ResultSet rs = stmt.executeQuery();
結果は、エスケープの時とまったく同じであった。MySQL 5.1でも直っていない…というか、これは仕様かもしれない。やはり、文字エンコーディングはアプリからDBまでそろえよう。

追記(2008/12/24 00:00)

へぼへぼCTO日記さんからトラックバックを頂戴した。Connector/JでサーバーサイドのpreparedStatementを使用するには、オプションuseServerPrepStmts=trueを指定しなければならないとのこと。手元の環境でテストしたところ、同オプションを指定したところU+00A5によるSQLインジェクションは再現しなくなった。ご指摘ありがとうございます。

追記(2009/07/17 14:00)

SH2さんのブログによると、MySQL Connector/J 5.1.8にてこの問題は修正されたようです。ありがとうございました。

2008年8月19日火曜日

session_set_save_handlerのパストラバーサルで任意コマンドの実行が可能

補足

この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2008年8月19日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

昨日の日記(session_set_save_handlerリファレンスマニュアルのサンプルにパス・トラバーサル脆弱性)で、PHPの公式リファレンスマニュアルに出ているsession_set_save_handlerサンプルにはパストラバーサル脆弱性があることを報告しましたが、その影響度について書き漏らしていて、影響度を過小に受け取られることに気がつきましたので補足します。

このパストラバーサルは情報漏えいよりは書き込み・破壊の影響の方が現実的というのはその通りなのですが、Web公開領域のファイルを書き換えられるというリスクを報告していなかった。ここで、HTMLやJavaScript、PHPスクリプトを書き込み、実行できるという問題があります。

以下のコード(a.php)で検証してみました。

// session_set_save_handlerのサンプルコード
// 以下は呼び出し部分
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
session_start();

$_SESSION['a'] = $_GET['a'];
echo "<body>done<body>";
?>
ご覧のように、クエリストリングaの値をそのままセッションに保存しています。非常に単純化していますが、現実のWebアプリケーションを極小化したモデルです。

ここで、Cookie PHPSESSIDの値を以下のようにセットします。b.phpの部分は、このサーバー上に存在するファイル名を指定します。
PHPSESSID=/../../../../../var/www/html/php/b.php
この状態で、以下のURLでa.phpを起動します。
http://host-name/php/a.php?a=<script>alert(document.cookie);</script>
セッションデータはb.phpとして格納され、内容は以下のようになります。
a|s:40:"<script>alert(document.cookie);</script>";
すなわち、今後b.phpにアクセスしたユーザは、ブラウザ上でJavaScriptが起動されることになります。

同様にして、PHPのスクリプトを書き込むこともできます。例えば、以下のようにa.phpを呼び出します。
http://host-name/php/a.php?a=<%3Fphp+echo`find`;%3F>
%3Fは、「?」を表します。すなわち、PHPスクリプト中で、バッククォートによりfindコマンドを実行するスクリプトが書き込まれたことになります。攻撃者はこのb.phpにアクセスすることにより、findコマンドを実行でき、同様にして、ターゲットのwebサーバー上で任意のコマンドを実行できることになります。

このように、書き込み可能なパストラバーサルは極めて危険な脆弱性であり、該当するアプリケーションは直ちに対策をとることをお勧めします。

2008年8月18日月曜日

session_set_save_handlerリファレンスマニュアルのサンプルにパス・トラバーサル脆弱性

補足

この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2008年08月18日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり

PHPのsession_set_save_handlerのリファレンスを眺めていて、ふと、これはパス・トラバーサルの脆弱性があるのではないかと思いました。

function read($id)
{
  global $sess_save_path;

  $sess_file = "$sess_save_path/sess_$id";       // ← ファイル名の組み立て
  return (string) @file_get_contents($sess_file);
}

function write($id, $sess_data)
{
  global $sess_save_path;

  $sess_file = "$sess_save_path/sess_$id";       // ← ファイル名の組み立て
  if ($fp = @fopen($sess_file, "w")) {
    $return = fwrite($fp, $sess_data);
...
session_set_save_handler("open", "close", "read", "write", "destroy", "gc");
...
ここで、readはセッションデータを読み出す関数、writeはセッション値を保存する関数で、session_set_save_handlerでセットしておくものです。コメントで「ファイル名の組み立て」と示している部分でファイル名をセットしていますが、変数$idの値(セッションID)の値が未検証のまま使われています。

問題は、PHP処理系にてセッションIDの値がどの程度チェックされるかです。よく知られているように、PHPにはSession Adoptionの問題があり、素のままの状態では外部からCookie PHPSESSIDにより指定されたセッションIDをそのまま受け入れます。私が色々な文字で試した範囲では、「<」、「>」、「'」、「"」に関してはチェックが行われており、これらの文字がPHPSESSIDに含まれていた場合には、セッションIDの再設定が行われました。一方、それ以外の文字、とくに「/」、「.」、「\」などは特にチェックされないまま素通ししてしまうので、パストラバーサルの脆弱性となります。

このサンプルを流用しているようなケース、あるいは類似の処理を行っている場合(session_set_save_handlerにて、ファイルによるセッションデータ保存を行っている場合)には、この問題の影響を受けます。

この問題の影響範囲ですが、情報漏えいの可能性は低いと考えられます。パス・トラバーサルの技法で任意のファイル名を指定することは可能ですが、たまたまPHPのセッション保存形式と適合する形式のファイルでなければ、読み出しは行われないからです。そのようなファイルがたまたまWebサーバー上に存在し、かつそのファイル名が類推できる場合に限られますが、そのようなケースは想定しにくいと考えます。

一方、ファイルの破壊(書き込み)については、権限さえあれば任意のファイルを指定して破壊できるので、ある程度の影響が考えられます。UNIX系のOS上でPHP(Apache)を実行するユーザの権限で書き込みが可能なファイルは一般的には限定されますが、権限設定がゆるい場合には影響を受けます。Windows上でPHPが稼動している場合には、影響はもう少し広いと考えられます。

対策について。Webアプリケーション側でこの問題に対応するには、さしあたっては、セッションIDの妥当性確認を行えばよいと思います。セッションIDが英数字のみで構成されているか、あるいは16進文字列として妥当であるかをチェックすれば、パストラバーサルは防げます。

また、この問題はPHPがSession Adoptionの問題があることに起因していますから、Strict Sessin Patchを適用すれば、上記問題も解消されると思います。しかし、その場合でも、防衛的意味でパス・トラバーサル対策としての文字種チェックはしておくべきでしょう。

session_set_save_handlerを使わない状態のPHPでは、パス・トラバーサルの問題は起きないようです。前述の中途半端な文字種チェックといい、session_set_save_handlerを使う場合と使わない場合の挙動の違いといい、ちょっと「イラっ」と来たことを告白します。

なお、この問題を一応脆弱性情報としてIPAに届出ましたが、独立したソフトウェア製品ではないという理由で不受理となりましたので、ここに公開し、PHPの開発者に注意を喚起するものです。

Windows上のPHP 5.2.6およびCentOS 5.2上のPHP 5.1.6で検証しました。

続く(session_set_save_handlerのパストラバーサルで任意コマンドの実行が可能)

2008年7月22日火曜日

そろそろWAFに関して一言いっとくか~三重苦を乗り越えてWAFが普及するための条件とは~

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2008年7月22日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
なお、この記事を書いた後、WAFはこの記事の予言(願い?)通りに進展したように思います。そのあたりの歴史については、こちらのインタビュー記事を参照下さい。
補足終わり

PCIデータセキュリティ基準(PCIDSS)がWAF(Web Application Firewall)について言及していることなどから、最近再びWAFへの関心が高まっている。一方、WAFは、一部のユーザや専門家に非常に評判が悪い。なぜ、そのようなことになるのか。本稿では、WAFの基本機能を説明した上で、その限界と運用上の問題を指摘し、今後のWAFの使い方について私見を述べる。

今回とりあげるWAFの基本機能は、以下の三種類である。
  • 入力値検査
  • 画面遷移のチェック
  • hiddenフィールド操作の防止

WAFの機能(1)入力値検査

すべてのWAFの備える基本機能は入力値検査である。これは、パラメタ(クエリストリング、POSTパラメタ、Cookieなど)に対するホワイトリストあるいはブラックリストによる検査を行うものだ *1。SQLインジェクションやXSSなどインジェクション系の脆弱性対策は、この入力値検査が基本となる。

入力値検査-ホワイトリスト検査

IPS(Intrusion Prevention System)が基本的にブラックリスト検査のみであるのに対して、WAFがホワイトリスト検査もできることは、WAFの特徴といえるだろう。ホワイトリスト検査は、Webアプリケーションの入力パラメタ一つ一つに対して、取りうる値の集合を正規表現などで定義し、検査の結果取り得る値から外れていたらリクエストをブロックするというものだ。前述のIPSに対するWAFの「優位」としてベンダーが大々的に宣伝することが多いのがこれだが、実際のサイトに適用するとなると以下のように問題が多い。

  • ホワイトリスト検査が可能なパラメタは限られている
  • ホワイトリスト検査の対象パラメタと検査内容は個別に指定する必要があり煩雑である
  • ホワイトリスト検査は本来アプリケーションで行うべきもの
  • ユーザビリティの低下を招く可能性
以下、順に説明しよう

ホワイトリスト検査が可能なパラメタは限られている

ホワイトリスト検査が可能なパラメタは、数値や英字など特定の文字種のみを受け入れるパラメータに限られる。具体的には郵便番号、電話番号、クレジットカード番号、ユーザID、メールアドレスなどだ。これら以外の自由記述形式のパラメタに対してはホワイトリスト検査はできない。

ホワイトリスト検査の対象パラメタと検査内容は個別に指定する必要があり煩雑である

前のところで説明したようにホワイトリスト検査可能なパラメタは限られており、かつパラメタ毎に文字種や文字列長などが異なっている。このため、WAFの機能として、URLごとのパラメタを列挙した上で、パラメタ毎に検査方法を定義できるようになっている場合が多い。中規模以上のWebアプリケーションには膨大な数のパラメタがあるため、この設定はかなり煩雑となる。

また、パラメタ毎の文字種などの仕様書があればよいが、なければアプリケーションをリバースエンジニアリングの手法で調べた上でWAFの設定を行う必要がある。文字種の制限が仕様書に記載されていることは期待できない。なぜなら、仕様書に書いてあるくらいならアプリケーション側で文字種チェックが実装されているはずであり、わざわざ手間を掛けてWAF側で設定する必要はないからだ。

最近の高機能WAFは「学習機能」と称してホワイトリストの自動設定ができるとうたっている場合が多いが、私の経験および見聞きした範囲では、学習機能がうまく動いて設定が自動化できたという話は聞いたことがない。最終的には人手によるチェックと修正が必要と考えた方がよいだろう。

ホワイトリスト検査は本来アプリケーションで行うべきもの

先にも少し触れたように、ホワイトリスト検査は本来アプリケーション側で行うべきものだ。郵便番号欄を例に説明すると、3桁および4桁の入力欄を用意して、数値以外の文字が入力されていればエラー表示して再入力を促すことになる。それくらいはアプリケーション側でやるべきだし、WAFでチェックすると後述のようにユーザビリティ的に問題となる可能性が高い

このように書くと、いやWAFで検査するのはラジオボタンなど選択式の入力欄に想定外の文字が入っている場合が主であって、郵便番号はアプリケーション側の検査でよく、この使い方であればユーザビリティ上の問題はないという反論がくるかもしれない。しかし今度は、ホワイトリスト検査が可能なパラメタのうち、どれがアプリケーションでの検査、どれがWAFでの検査とするかを詳細に調べ上げなければならない。これを学習機能で自動することは相当難しいだろう。というわけで、人手で設定するのは面倒だし、自動化には困難が伴う。

ユーザビリティの低下を招く可能性

前に書いたことと関連するが、ユーザの誤入力がWAFのホワイトリスト検査に引っかかった場合、ユーザに適切なナビゲーションを提供することは難しいだろう。そのためにユーザビリティが低下する可能性がある。

入力値検査-ブラックリスト検査

Webアプリケーションのパラメタのうち、ホワイトリスト検査ができない(しない)パラメタについてはブラックリスト検査をすることになる。この場合のブラックリスト検査の目的は、「アプリケーション要件としては禁止されていないが、脆弱性に対する攻撃(SQLインジェクションなど)が疑わしい入力をブロックする」ことになる。このため、ブラックリスト検査は、過剰検知と検知漏れの両方のリスクがある。

過剰検知の問題

ここでいう過剰検知(False Positive)とは、実際には攻撃目的の入力ではないのに、WAF攻撃とみなしてリクエストをブロックする場合を指す。Webアプリケーションの場合、アプリケーション要件的に禁止されていないものをブロックする*2わけだがら、ユーザにとっては不満の原因になる可能性が高い。

検知漏れの場合

検知漏れ(False Negative)とは、実際には攻撃を受けているにもかかわらず、見逃してリクエストを受け付ける場合を指す。すべての攻撃パターンを正規表現などで表現することは元々不可能であるし、検知漏れを少なくしようとすると、今度は過剰検知が増えるという結果になる。
ネガティブか、ポジティブか……それが問題だ
というわけで、LAC川口氏のコラムを引用させていただくことになるわけだが、脆弱性スキャナやIDSなど検査・監視系ツールの場合と異なり、過剰検知はユーザの実行を妨げ、ユーザビリティの低下に直結するため、少し緩めの(ブラック側に倒した)ルールにせざるを得ない。

そもそも入力時点検査での対応では無理がある - サニタイズ言うな

ここまで、ホワイトリスト検査とブラックリスト検査の特徴を説明してきた。ホワイトリスト検査は設定の手間が掛かることと、すべてのパラメタに適用可能ではない。ブラックリスト検査は過剰検知や検知漏れの可能性があり、総合的に判断して、入力値検査で完全な脆弱性対策とするのは無理がある。これは、セキュアWebアプリケーション開発の方法論としては既に結論が出た内容であって、XSSにせよ、SQLインジェクションにせよ、入力時ではなく、出力時(その値を使う時)に適切なエスケープなどにより対策すべき問題であるのに対して、WAFは出力時対応ができないからだ。言い換えれば、WAFはサニタイズ方法論で対応するものであって、元々限界があるのだ。

WAFの機能(2)画面遷移のチェック

すべてのWAFが備えているわけではないが、商用WAFの多くが、なんらかの画面遷移のチェック機能を持っている。これは、大きく分けて二種類あり、(1)外部からの入り口となるページをチェックする、(2)Webアプリケーション内の全ての遷移をチェックする、という方法がある。

まず、(1)の外部からの入り口のチェックについて説明する。右図は、Webで実装された業務システムを想定した画面遷移図である。入り口のページにログイン画面があり、その後は画面遷移に従って、業務メニューから各種の機能に遷移するという典型的な業務システムである。このようなアプリケーションの場合、外部からの入り口はログインページに限定すべきであり、その他のページに外部からダイレクトに遷移することは禁止するべきである。WAFによっては、この遷移制限の機能がある。

一見よさそうだが、このようなアプリケーションの場合、そもそも認証機能が正しく動いている限り、ログインページ以外には外部から遷移できないのであり、もしログインしていないユーザが遷移できたとすればそれは認証機能の重大な脆弱性である。脆弱性があるからWAFを導入するんだろうという突っ込みは予想できるが、こんな重要かつ基本的なところでWAFに頼らないと認証が正しく動かないのは問題であって、もし脆弱性があるならばWAFに頼らずに修正するしかないだろう。

一方、XSSやCSRFなど受動的攻撃への対策として、既にログインしている正規ユーザが外部から強制的に遷移させられるのを防止するという観点であれば、この機能に意味がある

次に、右の図は、ECサイトをモデル化したものである。この場合は、商品を選んでいる最中ではまだログインしておらず、かつ検索サイトなどからの遷移はむしろ歓迎するべきものであるので、WAFにより禁止するわけにはいかない。決済画面など一部のページは外部からの直接遷移を禁止してよい(禁止すべき)であるが、どのページは外部からの遷移を許可し、どのページは禁止するかという、一種のホワイトリストを定義することが煩雑となる。したがって、サイトの作りによっては、この「外部からの直接リンク禁止」機能を使える場合もあるが、多くのサイト(典型的にはECサイト)では、この機能を運用することはかなり労力が必要となる。


内部画面遷移をチェックするのも多大な労力が必要

画面遷移のチェック機能として、Webアプリケーション内部の遷移(狭義の画面遷移)をチェックできるものがある。そのためには、まず正しい画面遷移をホワイトリストとしてWAFに定義する必要がある。私は、コンサルタントとして非常に多くのWebサイトの検査に携わってきたが、画面遷移図がドキュメントとして完備しているサイトがそもそも少なく、タイムリーにアップデートされているサイトは皆無といってよかった。しかも、アップデートされた画面遷移図があったとしても、通常全ての遷移を網羅しているわけではなく、例外的な遷移(エラー時など)までは記述していないと思う。その理由は、例外的な遷移まで画面遷移図に書いていくと、非常に読みにくいものになってしまうからだ。仮に完璧な画面遷移図があっとしても、それをWAFに設定することは大変な労力であるし、Webアプリケーションの保守作業のたびにWAF側の設定をアップデートしないと、更新された機能が動かなくなる。

このような理由から、画面遷移のチェック機能は、現実には使われないケースが多い*3

WAFの機能(3)hiddenフィールド操作の防止

これもすべてのWAFが備えているわけではないが、商用WAFの多くが備えている機能として、いわゆる「hiddenフィード改ざん」の検知とブロックがある。すなわち、hiddenフィールドやCookie、ラジオボタンなどの選択肢の値がクライアント側で改変されてないかチェックし、改変されていたらエラーとするものである。

この機能の実装方式としては、私の知る限り二種類ある。一つは、hiddenフィールドやCookieの値を暗号化して改変できなくする、あるいはハッシュ技術により、改変されたことを検知・ブロックすると言うものだ。この実装方式は、クライアント側のJavaScriptと相性が悪いことが多く、暗号化あるいはハッシュ値が付与された値をJavaScriptで正しく読み込みできなくなる場合がある。

もう一つの実装方式は、WAF側でセッション管理を行い、hiddenの元の値を覚えておき、ブラウザからのリクエストとWAF側で記録した値とを比較するというものだ。一見よさそうな機能だが、ユーザが戻るボタンを操作すると、WAFで覚えている値とhiddenの値の食い違いが発生しそうだ。前述の画面遷移チェックと組み合わせて使うとよいのかもしれないが、設定が大変なためお勧めできない。

また、最近のWebアプリケーションでは、JavaScriptによりHTMLの内容をダイナミックに書き換えることが普通になっているわけだから、hiddenの書き換えをブロックこと自体が、Webアプリケーションの進化の方向とは衝突するものであるように思う。

WAFの三重苦とはなにか

ここで、WAFの三重苦とは何かを説明しよう。

大半の商用WAFがホワイトリスト方式を標榜しているが、そもそもホワイトリストというものは、予め正しい内容を設定しておく必要があり、その元となる正しい仕様書がないからアプリケーション側の作りこみがもれるのだ。その肝心なところを直視しないでWAFに頼ろうとするユーザは、WAF購入後にたちまち現実に直面するし、WAF(最近はワフというらしいが)に対する不満を募らせることになるのだ。WAFが安いものであればまだしも、非常に高価な製品であるだけになおさらだ。というわけで、WAFの三重苦とは、こうだ。

割高で あてにならぬも 負担増

なんだか、はてなハイクのようになってしまったが、高価で、設定に手間が掛る割には、これで万全と言うものではないということだ。

三重苦を乗り越えWAFが普及するための私見

ここで、まとめとしてWAFに関する私見を述べよう。

まず、WAFはホワイトリスト方式であるという主張はやめた方がよい。そんな主張をするから誤解を招くのだ。ホワイトリストの元となる正しい仕様は、仕様書に書いてないといけないし、仕様書に書いてあることはアプリケーションとして実装しなければならないのだ。だとすれば、WAFとアプリケーションでホワイトリスト検査の二度手間をやることになってしまう。

一方、WAFのブラックリスト検査については、私は元々懐疑的だったが、その考えが少し変わってきた。「サニタイズ言うな」に基づく現代的なセキュアWebアプリケーション開発では、アプリケーション側ではブラックリスト検査をしない。だから、WAFとアプリケーションとで検査の重複はなくなるのだ。実運用に影響を与えない程度の控えめなブラックリストをチューニングして、それを全パラメータ共通で使用する。そうすれば、手間も掛らないし、機械化されたSQLインジェクション攻撃程度であれば十分な効力がある。それでも運用に影響が出るパラメタもある(ブログや掲示板など)が、そのパラメタだけはブラックリスト検査を除外して、その代わりしっかり脆弱性検査をして対策もアプリ側でとっておけばよい。

Webアプリケーションの脆弱性対策は、アプリケーションの作りこみで行うべきであるが、アプリケーションが大規模になるほど対策漏れの可能性は大きくなる。上記のようなWAFの運用であれば、アプリ側の対策漏れに対するセーフティネットとして働くことが期待できる。その理由は、WAFの細かい設定をしないからであり、WAFの細かい設定をすればするほど、WAF自体の設定漏れを心配しなければならなくなる。それは本末転倒であって、WAFに対して割り切った使い方をした方がかえって安全ともいえる。

また、WAFの低価格化は必須だ・・・というより、ホワイトリスト機能を使わないのであれば、安いWAFで十分だ。そうすると、割高、負担増の二重苦が消え、残りは「あてにならない」だけが残るが、セキュリティ製品なんてファイアウォール、IPS、ウィルス対策ソフト・・・いずれをとっても「あてになる」ものはないのだから、WAFにだけ完璧を期するわけにもいくまい。ブラックリストでの防御という点ではIPSと機能がかぶるが、ことWebアプリケーションの脆弱性に関しては、ブラックリスト機能だけ使ってもWAFのほうが検知能力が高い。ここに、WAFの存在価値が見出せると私は考える。

最後に控えめな宣伝を。WAFの選定・導入に関する相談はこちらまで。

参考:ITproに書いたWAFの評価・比較レポートです WAFでWebアプリの脆弱性を守れるか:ITpro

2009/10/26追記

KCCSにて、10月29日(木曜)、11月26日(木曜)、12月17日(木曜)の3回にわたり、WAFのセミナーを実施することになりましたので、興味のある方は、こちらのリンク(アーカイブ)から内容確認の上お申し込みください。

*1 ホワイトリストとブラックリストについては、ホワイトリスト方式の優位は神話を参照されたい
*2 アプリケーション要件として禁止されているならホワイトリスト検査が可能だ
*3 これについては、WAFベンダーからも次のような証言がある。
APC Phase 2が最もセキュリティの高いものであることはいうまでもないが、すべてのページ遷移パターンまでもWAFに登録せねばならない。そうなると初期設定に相当の時間がかかり、運用開始後のコンテンツ変更時にも相当の労力と時間を要する。事実、BIG-IP ASMを採用している顧客の中でも、APC Phase 2を利用している顧客は、極めてコンテンツ変更の頻度が少ない数社しか存在しない。
WAFのセキュリティレベルとパラメータ設定より引用



6 件のコメント:

□ AzureStone (2008年07月23日 09:11)

初めましてAzureStoneです。記事を読みました。WAFは、実際にお使いになられたことはあります?

□ 徳丸浩(ockeghem) (2008年07月23日 09:56)
AzureStoneさん、こんにちは
私が使ったものと言っても、実際には私が指揮して、私自身が手を動かしてないものも含まれますが、以下はお客様に導入したものと実機で評価したものです。

AppShield
InterDo
Teros (現Citrix)
NetContinuum (現Barracuda)
SecureSphere Imperva
F-Secure Site Guard

ですね。こうしてみると、なくなったり、買収されたものが多いなぁ。メジャーなところでは、F5 BIG-IPは使ったことがありません。

□ AzureStone (2008年07月23日 10:06)
> 私自身が手を動かしてないものも含まれますが
あっ、なるほど。

ぐ、ぐぐ、具体的な製品名ありがとうございます。。
ちょっと驚きました。

NetContinuum (現Barracuda) とF-Secure Site Guard は、
ホワイトリスト・ブラックリスト両方いけますよ。
パターンパッチは、SiteGuardの方が種類が豊富でNetContinuum (現Barracuda)と比べて誤検知が少なかったです。

□ AzureStone (2008年07月23日 10:08)
F-Secure Site Guardは、買収ではなく業務移管するようです。

□ まっちゃだいふく (2009年09月04日 11:02)
誰がWAFのお金を出すか、そこも合わせて考えないといけない時期ですね。
■新規構築時
 開発にて設計上必要と考慮して購入
 → 開発費用として計上
■運用上時に問題として発覚時
 運用チームにて予算計上して購入
 → 通りにくい運用の予算計上

現実、今のWAFの興味を持っている人が後者なので、予算的にきびいいのかなぁ、って思います。

□ トムヤン (2010年03月19日 14:52)
最近、WAF製品の販売をしてまして、このサイト大変勉強になりました。おっしゃられたブラックリスト・ホワイトリストは運用や設定が手間と聞いていたので、ルールベース型WAFの「WAPPLES(ワップル)」をかついで販売をしようと思っています。もし、このへんの情報があれば教えてください。

2008年7月16日水曜日

ホワイトリスト方式の優位は神話~ホワイトリストとブラックリスト~

補足

この記事は旧徳丸浩の日記からの転載です(元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2008年7月16日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
なお、「ホワイトリスト」という用語の定義については、以下の記事の「第二種ホワイトリスト」に相当するとお考えください。この記事の投稿当時、ホワイトリストという用語の定義の揺れについては意識しておりませんでした。


補足終わり

近々WAF(Web Application Firewall)の話題を取り上げたいと思っている(→WAFの話題はこちら)。WAFの説明には決まってホワイトリストとブラックリストという用語が出てくる。しかし、WAFの宣伝やブログなどのエントリを読んでいると、ホワイトリストやブラックリストという言葉に対する誤解があるように見受けられる。そのため、WAFの話題の前に、この二つの用語の説明をしておきたいと思う。

ごく大雑把に言って、ホワイトリストは「怪しくない人・モノ」を列挙したもの、ブラックリストは「怪しい人・モノ」を列挙したものだ。これらのうち、日常生活でなじみのある用語はブラックリストだろう。クレジットカードの支払いを延滞すると「ブラックリスト」に名前が載り以降しばらくカードが作れなくなるとか、テロ組織のメンバーの名前が書かれた一覧表も「ブラックリスト」と呼ばれる。

最近話題の携帯コンテンツのフィルタリングについても「ホワイトリスト」方式と「ブラックリスト」方式の是非が議論された。この場合は、青少年の閲覧に問題ないサイトの一覧が「ホワイトリスト」、問題が想定されるサイトの一覧が「ブラックリスト」となる。

さて、問題はこの二つの方法論の使い分けだ。冒頭に述べたようにセキュリティ業界ではなにかとホワイトリストの人気が高いようだが、この傾向はWAF分野において顕著だ。以下は、とあるWAFの宣伝文句であるが、ホワイトリスト方式の利点が高らかにうたわれている。
・・・のWAF機能は、ポジティブ・セキュリティと呼ばれるホワイトリスト方式を採用している。これはポリシーによって正しいと定義されたトラフィックのみに、Webアプリケーションへのアクセスを許可する方式。不正アクセスをブラックリスト方式で識別するIDC(不正侵入検知システム)やIDP(不正侵入防御システム)では常にリストを更新する必要があるが、ホワイトリスト方式なら頻繁な更新は不要で、新しい攻撃に対する防御能力も高い。 引用元
一例のみ紹介したが、このような題材を探すには苦労しない。そして、そのような説明の多くが、ブラックリスト=古くて劣ったもの、ホワイトリスト=新しくて優れたもの、という調子だ。引用した文もそうなっているが、ホワイトリスト方式=ポジティブ、ブラックリスト方式=ネガティブという用語も(こう呼ぶ意味はあるのだが)ホワイトリスト方式の優位を印象付ける。

しかし、である。本当にホワイトリストが優れていて、ブラックリストが劣っているのであれば、法律かなにかでブラックリスト方式を禁止し、今後はホワイトリスト方式のみを採用するようにすべてのセキュリティベンダに強制すべきではないのか。現実にはそんなことはできないのであって、例えばウィルス対策ソフトをホワイトリスト方式で実装することは不可能だ。ホワイトリストとブラックリストにはそれぞれ長所と短所があって使い分けをすべきものであり、どちらが優れているとか劣っているというものではないのだ。

右の図はホワイトリストとブラックリストの位置づけを概念的に示したものだ。図のように、ホワイトリスト(WL)は「まず安全と考えられるもの」を列挙したもの、「ブラックリスト(BL)」は「安全でない可能性がかなりあるもの」を列挙したものとなる。そして、ホワイトリストとブラックリストのどちらにも載ってない中間部分が、「白黒はっきりしない中間領域」すなわちグレイゾーンとなる。

この図からもわかるようにWLとBLで示すことのできる領域は全体として一部であり、どうしてもグレイゾーンが大きくなる。すなわち、WLは判断を安全サイドに倒して「安全という保証のないものは全て排除する」もの、BLはカバー範囲を広くとることを重視して「明らかに怪しいもの以外は受け入れる」方法ということになる。この関係を以下に表として示した。


方式ホワイトリスト方式ブラックリスト方式
カバー範囲狭い広い
安全性高い危険なものを受け入れる可能性あり

これだけのことだ。ホワイトリスト方式の方が安全なことは確かだが、世の中全てホワイトリストで回せるはずがない。セキュリティの世界では守るべき対象の性質によってはホワイトリストが使える場合があって、その場合はぜひホワイトリストにしなさいというだけのことである。前述のように、ホワイトリストが使えない場合が現実には大半なので、(問題があるとは分かっていて仕方なく)ブラックリスト方式を使う。それだけのことだ。特に方法論自体の優劣とは関係ない。

ついでのように紹介して恐縮だが、大垣さんの書かれたホワイトリストはどう作る?は、ホワイトリスト神話の悪しき例と言わざるを得ない。
スクリプトインジェクション(XSS)防止にブラックリストが機能しない事は明らかです。ホワイトリストはどう作れば良いか参考となるリンクです。どう作るか書いておいても古くなる可能性が高いので、どこを参考に作れば良いか参考URLを書いておきます。
以下のリンクの情報からスクリプトのインジェクションがどのように行えるかを参考にホワイトリストを作れば概ね間違いないと思います。

Follow up:
XSS Cheat Sheet
http://ha.ckers.org/xss.html

スクリプトインジェクション手法の中でも有名な手法を集めているサイトです。XSSロケータと呼ばれている文字列はスクリプトインジェクション脆弱性検出に重宝します。よくある脆弱性であればこの文字列で簡単に検出できます。
[ホワイトリストはどう作る?より引用]
大垣さん、これではホワイトリストではなくて、ブラックリストそのものです。
一方、興味深いことに、金床さんの書かれたウェブアプリケーションセキュリティには、同じ題材を取り扱っているが、その記述はまるで異なる。
WAFを使用しブラックリスト方式のシグネチャマッチングによってXSS対策を行う場合、攻撃を完全に防ぐことは不可能である。これはXSSを引き起こす可能性のある文字列が非常に多岐にわたるためだ。このことは非常によく知られたドキュメントであるXSS Cheat Sheetを見るとよくわかる。【中略】望ましい対策として、パラメータごとにホワイトリスト式のチェックを行う方法が考えられるが、残念ながら多くのウェブアプリケーションではホワイトリストをきちんと定義することが難しい【中略】従って、WAFを使ってXSS攻撃を完璧に防ぐことは期待できない。
[ウェブアプリケーションセキュリティ(P92~P94)より引用]
同書の書評でも述べたが、オープンソースのWAFの開発者としてホワイトリストとブラックリストの両方に真剣に向き合ってきたからこそ書ける、正確かつ誠実な記述である。

金床さんのWAFの話題が出たところで、次回はWAFの説明に続く・・・そろそろWAFに関して一言いっとくか~三重苦を乗り越えてWAFが普及するための条件とは~



1 件のコメント:

_ yohgaki (2008年08月20日 16:50)
大垣です。手抜きエントリを見事に誤解されたので追記しました。
http://blog.ohgaki.net/-7

それから、ホワイトリスト方式とブラックリスト方式の考え方は私の考えとは大きく異なります。ホワイトリスト型、ブラックリスト型のセキュリティ対策では分かり辛いように思えるので個人的には能動的(プロアクティブ)と受動的(リアクティブ)なセキュリティ対策と言う方が好みですが、バッファーオーバーフローをバッファーオーバランと言うような物なのでホワイトリスト型、ブラックリスト型の対策として書いています。
http://blog.ohgaki.net/-13

ところでXSS Cheat Sheetは読まれましたか? アレを読んでそれでもブラックリスト方式でXSSを防げる、と信じられる方はかなりの重症だと思います。なので、読まれていないと思います。あのエントリはホワイトリストを自分で作る事を考えてもらえるように書いています。XSS Cheat Sheetを読まないと意味がありません。

2008年6月2日月曜日

SQLエスケープにおける「\」の取り扱い

補足

この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2008年6月2日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり


昨日のエントリ(徳丸浩の日記 - そろそろSQLエスケープに関して一言いっとくか - SQLのエスケープ再考)は思いがけず多くの方に読んでいただいた。ありがとうございます。その中で高木浩光氏からブクマコメントを頂戴した
\がescape用文字のDBで\のescapeが必須になる理由が明確に書かれてない。\'が与えられたとき'だけescapeすると…。自作escapeは危うい。「安全な…作り方」3版で追加の「3.失敗例」ではDBで用意されたescape機能しか推奨していない
このうち、まず「\」のエスケープが必須となる(MySQLやPostgreSQLで)理由を説明しよう。

「\」をエスケープしないと処理がおかしくなる

MySQLにおいて、文字列「\n」は改行を意味する。その他、\に続く文字によって、様々な制御文字などを表現できるようになっている。
このため、ユーザがたまたま入力欄などから「\n」と入力した際に、エスケープしないままだと、ユーザの意図に反して「\n」が改行に化けることになる。また、エスケープシーケンスとして定義されていない場合、たとえば「\x」は単に「x」を表すと規定されているので、「tokumaru.org大安売り¥100-」が「tokumaru.org大安売り100-」となり、「¥」が欠落してしまう。これはユーザの意図ではない。
このため、「\」を「\\」エスケープすることにより、上記のような文字化けを防ぐ必要がある。これが、そもそも「\」のエスケープが必須となる理由で、セキュリティ上の要求がなくても、必要な処理である。

「\」のエスケープもれによるSQLインジェクション

上記に加えて、「\」のエスケープが必要な状況でそれがもれている場合、SQLインジェクション脆弱性の原因となる。高木氏が指摘しておられるように、「\'」という入力に対して「'」のみエスケープすると、「\''」という文字列になる。前半の「\'」で「'」を表すので、末尾の「'」がエスケープされないで残ってしまう。つまり、文字列リテラルを終端できる。前回指摘したように、これによりSQL断片を埋め込むことが可能となる。この例だと分かりにくいので、もう少し現実的な攻撃パターンで説明しよう。
SELECT * FROM XXX WHERE NN='$id'

$id として \'or 1=1# が入力されると

\'or 1=1#
  ↓ エスケープ
\''or 1=1#

元のSQLに適用すると、

SELECT * FROM XXX WHERE NN='\'' or 1=1#'

すなわち、SQLの構文が改変された
上記でデータベースとしてはMySQLを想定している。「#」はMySQLでコメントを表すので、行末の「#'」は無視される。

Shift_JISでの問題

ここまでならまだよい。データベースの種類によっては「\」のエスケープを忘れないようにしようで済む。ところが、文字「\」を表す文字コード0x5cがShift_JISの2バイト目にも現れうることから話がややこしくなった(一方、「'」を表す0x27の方はShift_JISの二バイト目に現れない)。0x5cを二バイト目に含む文字は多数あるが、例として以下を紹介する。
ソ(835c)
能(945c)
表(955c)
予(975c)
上記のように出現頻度の高い文字が含まれている。言語処理系やデータベースエンジン、APIなどに日本語処理の不完全な部分があると、SQLインジェクションの可能性が出てくる。

データベースエンジンの日本語処理が不完全な場合

この場合は、「表'」のような組み合わせによりSQLインジェクションができる可能性がある。
'
0x950x5c0x27
↓フロント側でのエスケープ処理
''
0x950x5c0x270x27
↓データベース側の解釈
0x950x5c0x270x27
0x95\' で'一文字' がエスケープされずに余る

このように、末尾の「'」がエスケープされない状態となり、SQLインジェクション脆弱性が生まれる。

フロント側の日本語処理が不完全な場合

フロント側(言語処理系)の日本語処理が不完全な場合も「表'」の処理において「'」のエスケープ抜けが発生する
'
0x950x5c0x27
↓フロント側でのエスケープ処理(0x5cと0x27をそれぞれエスケープ)
0x950x5c0x5c0x270x27
↓データベース側の解釈
0x950x5c0x5c0x270x27
「表」一文字\'で一文字' がエスケープされずに余る

上記はShift_JIS固有の現象であるので、できるだけShift_JIS以外の文字エンコード、例えばUTF-8を使うとよい。しかし、ケータイブラウザのようにShift_JISのみ受け付けるものや、エンタープライズ系の応用では文字化けを避ける目的でShift_JISを要求される場合もある(入出力時に文字コード変換して処理はUnicodeに統一する手もあるが、わずらわしい場合もあるだろう)。

PostgreSQLの対応

PostgreSQLでは上記の問題に対応するために、バージョン8.1.4(2006年5月24日リリース)では、以下のような変更が行われた
  • 常にサーバ側で無効なコードのマルチバイト文字を拒否するように修正された
  • 文字列リテラル中の安全でない「\'」を拒否する機能が追加された
  • 以下略
上記については、ITproへの石井達夫氏による寄稿(【PostgreSQLウォッチ】第27回 SQLインジェクション脆弱性を修正,日本語ユーザーに大きな影響)が詳しい。簡単に要約すると、「\'」形式のエスケープを禁止・エラーにし、「''」方式(ISO標準)に限定できるようにした。これにより、Shift_JISの二バイト目の0x5cにまつわる「'」のエスケープ抜けを防止するというものである。但し、後方互換性の確保のため、backslash_quoteという設定パラメタが用意され、これが on の場合には、従来どおり「\'」形式のエスケープを許容する。

pg_escape_stringの挙動調査

冒頭に紹介した高木浩光氏のブクマコメントの後半は、自作のエスケープではなくDBで用意されたエスケープ機能を利用するようにという指摘であった。まことにその通りで、私のブログを読むと、自作のエスケープを推奨しているようにも読めるが、それは良くない。
それでは、PHPで用意されているpg_escape_stringは期待通り動作するのだろうか。簡単なスクリプトで検証してみた。
検証用スクリプト(PHP)

$cn = pg_connect("host=localhost user=xxxx password=xxx";
echo pg_escape_string($cn, "表\\'");

実行結果1 (standard_conforming_strings = on の場合)
表\''

実行結果2 (standard_conforming_strings = off の場合)
表\\''
PHP言語側のエスケープの都合で紛らわしいが、入力文字列は「表\'」である。前回紹介したstandard_conforming_stringsの設定を正しく反映して、onの場合は「表\''」(「\」のエスケープをしない)、offの場合には「表\\''」(「\」、「'」ともエスケープする)結果となっている。いずれの場合にも「'」は「''」とエスケープされるので、backslash_quoteの設定には依存しない。素晴らしい。

DBD::PgPPの場合

こんどはPerlでの例。PerlからPostgreSQLを利用する場合には、DBIとDBD::Pgの組み合わせが利用される・・・と思うのだが、筆者の環境では中々DBD::Pgがインストールできなかったので、代わりにDBD::PgPPを使って検証してみた。PgPPはピュアPerlで記述されたPostgreSQL用インターフェースである。DBD::PgPP中のquote()のソースを見ると、文字の変換部は以下のようになっていた(バージョン0.05)。
$s =~ s/(?=[\\\'])/\\/g;
return "'$s'";
正規表現の「?=」はゼロ文字の先読み表明というやつで、後ろに「\」か「'」が続くゼロ文字にマッチする。すなわち、「\」と「'」はそれぞれ「\\」と「\'」にエスケープされる。これはいただけない。standard_conforming_stringsもbackslash_quoteも無視されている。
これは恐らくDBD::PgPPの完成度があまり高くないということなのだろう。従って、自作のエスケープをせずにDBで用意されたエスケープ機構を使えというガイドラインは一般論として正しいと思うが、上記のような例もあるので、初めて使う前に簡単なテストをしておけば安心できる。

まとめ

  • 「\」のエスケープを要求するデータベースは日本語処理に特に注意
  • 例えば、Shift_JISを避ける
  • 自作のエスケープを避け、DBにて用意されたものを使う
  • その場合でも過信は禁物で、できるならチェックしてから使うとよい
参考:WASForum Conference 2008講演資料「SQLインジェクション対策再考」

2008年6月1日日曜日

そろそろSQLエスケープに関して一言いっとくか~SQLのエスケープ再考~

補足

この記事は旧徳丸浩の日記からの転載です。元URLアーカイブはてなブックマーク1はてなブックマーク2
備忘のため転載いたしますが、この記事は2007年11月26日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり


本稿ではSQLインジェクション対策として、SQLのエスケープ処理の方法について検討する。
最近SQLインジェクション攻撃が猛威を振るっていることもあり、SQLインジェクションに対する解説記事が増えてきたようだが、対策方法については十分に書かれていないように感じる。非常に稀なケースの対応が不十分だと言っているのではない。ごく基本的なことが十分書かれていないと思うのだ。
SQLインジェクション対策には二通りある。バインド機構を使うものと、SQLのエスケープによるものだ。このうち、SQLのエスケープについて、十分に書かれているテキストが見当たらないのだ。このため、自分で書いてみようと思う。
IPAの「安全なウェブサイトの作り方改訂第三版」ではSQLのエスケープについて以下のように説明されている。
1)-2 バインド機構を利用できない場合は、SQL 文を構成する全ての変数に対しエスケープ処理を行う
解説 これは、根本的解決 1) のバインド機構を利用した実装ができない場合に実施すべき実装です。
 利用者から入力されるパラメータや、データベースに格納された情報などに限らず、SQL 文を構成する全ての変数や演算結果に対し、エスケープ処理を行ってください。エスケープ処理の対象は、SQL文にとって特別な意味を持つ記号文字(たとえば、「'」→「''」、「\」→「\\」 など)です。
 なお、SQL 文にとって特別な意味を持つ記号文字は、データベースエンジンによって異なるため、利用しているデータベースエンジンに応じて対策をしてください。データベースエンジンによっては、専用のエスケープ処理を行うAPIを提供しているものがあります(たとえば、Perl ならDBIモジュールのquote()など)ので、それを利用することをお勧めします。

引用した部分は、一般的な内容を網羅しているものの不満もある。短い文章の中で「など」が3回も現れることに象徴されるように、あいまい性の残る文章となっている。これはデータベースの製品依存のところでやむを得ないというのはよく理解できるのだが、結果として、読者に「データベースエンジンのマニュアルを読め」と言っているの等しい。それでいて、読者には、マニュアルのどこをどう調べたらよいかまでは示していない。「エスケープ処理の対象は、SQL文にとって特別な意味を持つ記号文字」と書かれているが、この説明だと、「;」や「=」、空白までエスケープしなければならないと誤解する読者が出てくるかもしれない。実際には、これらの文字をエスケープする必要はないし、SQLの標準規格には、そもそもこれらの文字をエスケープする手段が用意されていない。エスケープの必要がないので手段も用意されていないのだ。
そこで、「安全なウェプサイトの作り方」を補完するような形で、もう少しSQLエスケープについて書き足してみたい。

SQLインジェクションのおさらい

SQLインジェクション攻撃では、SQLに渡すパラメータ部分にSQL断片を挿入し、SQLの意味を書き換えることによって行われる。 バインド機構を使わずに自前でSQLを組み立てる場合、SQLに対するパラメータはSQLのリテラル(定数)の形で渡される。
リテラルには複数の型があるが、通常問題になるのは数値と文字列である。
数値リテラルの場合:
SELECT * FROM XXXXX WHERE NNUM=●●●      -- ●●●は数値、例えば 123
文字列リテラルの場合:
SELECT * FROM XXXXX WHERE EID='■■■'      -- ■■■は文字列、例えば S853
なぜリテラルを構成する文字列(●●●や■■■)を操作することでSQLの構文まで変わるのか。それは特殊記号などでリテラルを終端させ、その後にSQL断片を埋め込むからだ。
数値リテラルを終端させるには、数値以外の文字を加えればよい。
例: 123OR TRUE         -- 数値リテラル123の後に、OR TRUE が続く。123 OR TRUEと同じ
文字列リテラルを終端させるには、単一引用符「'」を加えればよい
例: A'OR'A'='A

この例を先のSQLに適用すると、

SELECT * FROM XXXXX WHERE EID='A'OR'A'='A'

となる。つまり、WHERE句は常に真となる。

SQLインジェクション対策の基本的な考え方

すなわち、SQLインジェクション対策の方針としては、リテラルを勝手に終端させないようにすることが必要なのだ。そして、この処理は数値リテラルと文字列リテラルとでは、方法が異なる。
数値リテラルの場合は、数値以外の文字が出てきた時点でリテラルの終端となるので、数値としての妥当性検証を行うことになる(数値項目に対するSQLインジェクション対策のまとめ参照) 。
一方、文字列リテラルの場合は、文字列リテラルのエスケープを行えばよい。SQL92などSQLの標準規格で規定しているのは、単一引用符「'」のエスケープであるが、データベースの種類によっては、円記号(バックスラッシュ)「\」のエスケープも必要となる。

商用データベースの場合

商用データベースの場合は、SQL標準に従い、単一引用符「'」を「''」と重ねる処理を行う。OracleSQL ServerIBM DB2についてはリファレンスと動作の両方で確認した。

オープンソース・データベースの場合

オープンソースのデータベースとして広く普及しているMySQLの場合、文字列リテラル中にC言語風の「\」を使ったエスケープシーケンスが記述できる。従って、文字列リテラル中に「\」自体を記述する際には、「\\」とする必要がある。一方、単一引用符「'」は「''」としてもよいし、「\'」としてもよい。その他、二重引用符「"」が利用できるなど自由度が高い。
オープンソース・データベースのもう一方の雄PostgreSQLの場合は事情が少し複雑となるが、デフォルトではMySQLと同じで、単一引用符と円記号(バックスラッシュ)の両方をエスケープする必要がある。しかし、設定パラメータstandard_conforming_stringsがonの場合(デフォルトはoff)は、Oracleなどと同じ挙動となる(現実にはもう少し複雑だが稿をあらためて説明したい)。
まとめると以下のようになる。

データベース元の文字エスケープ後
Oracle
MS SQL
IBM DB2
'''
MySQL
PostgreSQL
''' または \'
\\\

なお、念のため補足すると、エスケープ処理はセキュリティ対策のために行うものでは元々なく、与えられたパラメータに対して正しく処理を行うために必要な処置である。入力に「'」や「\」が使えないと不便で仕方がないし、現実的に不具合が生じるだろう。

その他のデータベース製品の場合はどうしたらよいか

今回説明した内容にもっとも近い記述があるドキュメントとしては、佐名木智貴氏の近著「セキュアWebプログラミングTips集(ソフト・リサーチ・センター)」がある。同書では、SQLエスケープの基本として
「'」は、「''」(シングル・クォート2個)にエスケープ処理することで、SQLインジェクションから防御することができる(同書P210)
とした上で、
mySQLとPostgreSQLの場合のSQLインジェクション対策として、入力データをSQL文の文字列リテラルとして使う場合、「'(シングル・クォート)」と「\」をSQLエスケープすること(同書P213)
と指摘している。
非常に丁寧な仕事ぶりで好感を持った。ただ、同じページの以下はいただけない
筆者の知らないデータベース・ソフトウェアでは、SQLが拡張され、それ以外のメタキャラクタもあるかも知れない。ぜひ読者諸氏には今一度、自分の使っているデータベース・ソフトウェアのSQLリファレンスを通読することを推奨する(同書P213)。
「SQLリファレンスを通読」とは・・・無茶言うなよと思う。
実際には通読する必要はなく、「リテラル」、「定数」、「文字列」などのキーワードを手掛かりに、文字列リテラルの項を探すとよい。本稿で引用したリファレンスもこのようにして探したものである。
Oracleの場合を例にマニュアルの見方を説明しよう。題材として、Oracle10gのオンラインマニュアルを利用する。
まず、目次から「リテラル」を探すと、「2 Oracle SQLの基本要素」に「リテラル」や「テキスト・リテラル」という項が見つかる。「テキスト・リテラル」の項を読むと、
  • cは、データベース・キャラクタ・セットの任意の要素です。リテラル内の一重引用符(')の前には、エスケープ文字を付ける必要があります。リテラル内で一重引用符を表すには、一重引用符を2つ使用します
  • ''は、テキスト・リテラルの始まりと終わりを示す2つの一重引用符です。
このように、エスケープの必要な文字は一重引用符「'」であること、エスケープの仕方は一重引用符を2つ使用することであることがわかる。他のデータベース・ソフトウェアでも、同様に探すことができるだろう。
本稿を参考に正しいSQLインジェクション対策を実施していただきたい。

続く(徳丸浩の日記 - SQLインジェクション対策 - SQLエスケープにおける「\」の取り扱い)
参考:WASForum Conference 2008講演資料「SQLインジェクション対策再考」

追記(2010/03/26)

このエントリを書いた後、IPA非常勤研究員として、SQLインジェクションの正しい対策方法について調査・検討しました。その成果は「安全なSQLの呼び出し方」という冊子(安全なウェブサイトの作り方別冊)という形にまとめられました。ぜひご活用いただければと思います。ダウンロードはこちらから。

本日のツッコミ(全3件)

_ 佐名木 (2008年06月24日 21:59)
mySQL での「\」のエスケープについて知ったのは、(私にとっては無知の知の領域にある)mySQL のリファレンスを通読している時でした。私には衝撃的でした。無意味だと思うからです。「'」→「''」だけで十分なのに、「\」を導入する必要性を感じないからです。
なので、読者の人が使っているかも知れない私の知らない DB については、DB 開発者が自由に拡張しているかも知れないから注意してね。
というつもりなのです。

出版までにちゃんとまとめなくて中途半端だったので、次回は正規表現(Like演算子)の時のエスケープについてお願いします。
といっても正規表現の使用場面を考えると、そもそもインデックスを張っていないカラム対象が多いのでそれほど問題にはならないとは思いますが、プログラミング書法という観点でもお願いしたいですね。

_ momo (2010年05月06日 21:24)
クオートをエスケープした内容をSQLに格納するのはいいが、次にDBからその内容を利用するとき、エスケープは消え、クオートは裸のままであることをおわすれなく。

_ Nkzn (2010年05月27日 14:08)
↑セカンドオーダーSQLインジェクション?っていうんでしたっけ?