[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

徳丸浩の日記


2008年10月29日

_書籍「はじめてのPHPプログラミング基本編5.3対応」にSQLインジェクション脆弱性

id:hasegawayosuke氏にそそのかされるような格好で、「はじめてのPHPプログラミング基本編5.3対応」という書籍を購入した。

本書は、ウノウ株式会社下岡秀幸氏、中村悟氏の共著なので、現役バリバリのPHP開発者が執筆しているということ、下記のようにセキュリティのことも少しは記述されているらしいという期待から購入したものだ。

目次から抜粋引用
07-07 Webアプリケーションのセキュリティ [セキュリティ]
08-04 データベースのセキュリティ [SQLインジェクション]
09-13 セキュリティ対策 [セキュリティ]

本書をざっと眺めた印象は、「ゆるいなぁ」というものであるが、その「ゆるさ」のゆえんはおいおい報告するとして、その経過で致命的な脆弱性を発見したので報告する。

問題の報告

それは、本書P280に登場する「SQLインジェクション対策用の関数(dbescape)」だ。この関数を本書から引用する。

// SQLインジェクション対策用の関数
function dbescape($sql, array $params)
{
    foreach ($params as $param) {
        // パラメータの型によって埋め込み型を変える
        switch (gettype($param)) {
        case "integer":
        case "double":
            $replacement = $param;
            break;
        case "string":
            // 文字列の場合はエスケープ処理をおこなう
            $replacement = sprintf("'%s'", sqlite_escape_string($param));
            break;
        default:
            die("パラメータの型が正しくありません");
        }

        // SQLを置換し、パラメータを埋め込む
        $sql = substr_replace($sql, $replacement, strpos($sql, "?"), 1);
    }

    // すべてパラメータを埋め込んだSQLを返す
    return $sql;
}

この関数は、「穴埋め形式のSQL文字列と、埋め込むパラメータの配列を受け取り、必要なエスケープ処理を施したSQL文字列を返します」とのことで、以下のように用いる。

$sql = dbescape("SELECT COUNT(id) FROM friend WHERE from_name = ? AND to_name = ?",
                array($from_name, $to_name)); 

これに対して、$from_name = "Johnson"、$to_name = "M'Intosh" として上記を実行すると、以下のような文字列が返る

SELECT COUNT(id) FROM friend WHERE from_name = 'Johnson' AND to_name = 'M''Intosh'

すなわち、バインド機構を自前で実現したようなインターフェースである。

この実装を見た瞬間、違和感を感じた。これは書式文字列の処理に属するものであるので、通常は書式文字列($sql)を左から調べて、書式記号(?)が出てくるたびに、対応するパラメータの処理を行うのが定石的な実装だと思う。そうでないと(上記の場合は出てこないが)書式などのエスケープを上手く処理できない。しかるに、引用した関数では、パラメータの方を調べながら、対応する書式記号(?)を探している。しかも、未処理の部分と処理済の部分がごちゃまぜになっているので、まずいことが起こりそうである。

そう、この関数にはバグがある。パラメータとして"?"を含む文字列を与えた場合、元の穴埋め式SQLに存在した"?"と、新たに埋め込まれた"?"がごっちゃになる。試してみよう。先の例に、第一パラメータとして"?"、第二パラメータとして"AAA"を与えた場合の処理の流れは以下のようになる

0:SELECT COUNT(id) FROM friend WHERE from_name = ? AND to_name = ?
                                                 ↑ '?' に置き換え
1:SELECT COUNT(id) FROM friend WHERE from_name = '?' AND to_name = ?
                                                  ↑ 'AAA' に置き換え
2:SELECT COUNT(id) FROM friend WHERE from_name = ''AAA'' AND to_name = ?

ご覧のように、第一パラメータの変換結果である '?' から、さらに?部分が'AAA'に置き換わることから、意図した結果を得られない。しかも、右側の"?"があまってしまい、SQLの文法違反となる。

それだけならまだよいのだが、AAAの部分に着目いただきたい。この部分は外部から与えた文字列なので、シングルクォートで囲まれてなければならないのだが、上記の過程で、文字列リテラルからはみ出した、すなわちSQL文の式の一部として解釈される状態となった。この時点でSQLインジェクション脆弱性といえる(ライオン(=外部からの文字列)が檻(=文字列リテラル)から抜け出した状態)。

従って、AAAの代わりにSQL文をセットしてやれば、任意のSQLが実行できることになる。やってみよう。今度は第二パラメータとして"or 1=1--"をセットしてやる。変換後のSQLはこうなる。

SELECT COUNT(id) FROM friend WHERE from_name = ''or 1=1--'' AND to_name = ?

本書で想定しているRDB(SQLite)では、--は標準SQL同様コメントとなるので、--から先は無視される。すなわち、SQLの意味が書き換えられた。SQLiteではUNIONをサポートしているし、更新系SQLでは複文をサポートするようなので、様々な悪用が可能となる。

教訓

あらゆるバグは脆弱性になり得る

どうすべきだったか

汎用的なsqlエスケープ関数を用意して、対策をこの関数にカプセル化しようという心意気はよかったのだが、あいにくこの関数にバグがあって、意図がかえって仇となる結果となった。では、どうすればよかったか。

思うに、バインド機構に似た機能を自作しようというのが間違いで、そんなに簡単にできるものではない。PHPのsqlite_xxxx系の関数にはバインド機構が用意されていないようだが、PDOを利用することで、SQLiteでもバインド機構が利用できる。

あるいは、SQLiteの使用をあきらめ、MySQLを使ってもよかった。本書のカバーには「MySQL(データベース)」とある(これはCD-ROMにMySQLが添付されているということらしい)。WindowsでもMySQLは動作するし、実務でも利用機会はMySQLの方がずっと多いだろう。MySQLであれば、mysqli系の関数でバインド機構が利用できる。さらに大切なこととして、「SQLインジェクション対策は原則としてバインド機構を用いるべし」という原則を教えることもできるのだから。

本日のリンク元
その他のリンク元
検索


[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

ockeghem(徳丸浩)の日記はこちら
HASHコンサルティング株式会社

最近の記事

最近のツッコミ

  1. りゅう (10-18)
Google