なぜPHPアプリにセキュリティホールが多いのか?

第5回 まだまだ残っているSQLインジェクション

この記事を読むのに必要な時間:およそ 4 分

正しいSQLインジェクション対策

正しいSQLインジェクション対策には2種類の方法があります。

  • すべての変数をエスケープする対策
  • すべてのクエリをプリペアードクエリとして実行する対策

すべての変数をエスケープする対策

この方法はすべてのデータベースに利用できる対策です。文字列,整数などデータ型に関わらず変数すべてを文字列としてエスケープすることにより,SQLインジェクションを100%防ぐことが可能となります。例えば,PostgreSQLのSQL文を生成する場合,以下のようにすべてのパラメータを文字列して処理・生成します。

$sql = "UPDATE user SET name = '".pg_escape_string($_POST['name'])."', age  = '".pg_escape_string($_POST['age'])."' WHERE id = '".pg_escape_string($_SESSION['USERID'])."';";

一部のドキュメントなどでは,addslashes関数を利用するなど,間違ったエスケープ方法を推奨しているケースがあるので注意しなければなりません。addslashes関数や置換関数(strtr, preg_repalce等)を使用した方法ではSQLインジェクションが可能になる場合があります。必ずデータベースインタフェースが提供しているエスケープ関数を利用するようにします(日本語などマルチバイト文字のエンコーディングの仕様が原因で,機械的な置換を行うとSQLインジェクションが可能となります。特にSJISは明示的にエンコーディングを意識してエスケープしないと,データベースサーバ側では意図したSQL文か攻撃用の文字列であるのかまったく区別ができません)。

テーブル名,フィールド名を動的に決定している場合,エスケープは役に立ちません。このようなクエリを生成することはあまりないと思いますが,もし動的にテーブル名,フィールド名などを決定している場合,あらかじめ定義している文字列と一致しているか確認します。ユーザ入力値のテーブル名やフィールド名をチェックなしに直接クエリに使用すると簡単に不正なSQL文を実行可能です。

すべてのクエリをプリペアードクエリとして実行する対策

プリペアードクエリとは,実行するクエリをあらかじめパースし,パラメータを渡すだけでクエリの実行を行えるようにするDBMSの機能です。すべてのDBMSがプリペアードクエリをサポートしていないので,プリペアードクエリをサポートしているDBMSを利用している場合にのみ使用できる対策です。プリペアードクエリに渡されるパラメータはすべて値として処理されます。このため,プリペアードクエリのみでクエリを実行するとSQLインジェクションは不可能になります。

$res = pg_query_params($conn, 'UPDATE user SET name = $1, age = $2 WHERE id = $3', array($_POST['name'], $_POST['age'], $_SESSION['USERID']));

どちらかというとプリペアードクエリを利用したほうが安全性が高く(エスケープする方法ではエスケープ漏れのリスクが高い),後でコード監査を行う場合も容易に監査できます。プリペアードクエリが利用可能な場合はプリペアードクエリを利用するほうがよいでしょう。

しかし,プリペアードクエリだからといって安心できない場合もあります。データベースアクセス抽象化ライブラリを利用する場合,プリペアードクエリのようなインターフェースをサポートしていても,実際はプリペアードクエリでない場合があります(PDO,Zend Frameworkなど)。このようなライブラリを利用している場合,ライブラリの不備や利用方法に誤りがあるとSQLインジェクションに脆弱となる場合があるので注意が必要です。

まとめ

SQLインジェクションはすべての変数をエスケープするか,すべてのクエリをプリペアードクエリとして実行すれば100%防げる脆弱性です。ソースコードからこの2種類の対策が完全に行われているかチェックするのは非常に容易です。XSS等のJavaScript関連の脆弱性に比べ,SQLインジェクション対策は単純かつ明快です。

著者プロフィール

大垣靖男(おおがきやすお)

University of Denver卒。同校にてコンピュータサイエンスとビジネスを学ぶ。株式会社シーエーシーを経て,エレクトロニック・サービス・イニシアチブ有限会社を設立。
オープンソース製品は比較的古くから利用し,Linuxは0.9xのころから利用している。オープンソースシステム開発への参加はエレクトロニック・サービス・イニシアチブ設立後から。PHPプロジェクトでは,PostgreSQLモジュールのメンテナンスを担当している。

URLhttp://blog.ohgaki.net/

著書

コメント

  • preg_match のバリデーション例

    preg_match の不完全なバリデーションの例の答えですが、以下の説明が間違っています。

    「$_GET['id']中の最初の1行目が数字だけで構成されているかチェックしています。2行目以降にどのような文字列が入っていても構いません。」

    私の調べた限りでは、2行目以降もチェックされます。
    D 修飾子を付けない場合、最後の文字が改行でもマッチするというだけです。

    つまり、preg_match('/^[0-9]+$/', $_GET['id']) では、"1\n" は TRUE ですが、"1\n;SELECT * FROM product" は FALSE です。

    preg_match() で調べる文字列に改行が含まれない場合、D 修飾子を付けた方が安全だとは思いますが、この問題では「バリデーションには明らかな間違いがある」とは言えないのではないでしょうか。

    この問題では、register_globals = On の環境で $id が上書きされて SQL インジェクションが発生するという可能性の方が高いように思います。

    Commented : #2  komura (2007/06/10, 11:03)

  • はじめまして。

    はじめまして。
    記事を拝見し、すこし思ったのですが
    > なぜPHPアプリにセキュリティホールが多いのか?
    に関しては、「まずいサンプルが多い」のが一番の原因じゃないでしょうか。
    本記事の場合、非常に要点を抑えておられるとは思うのですが、サンプルコードの

    if (preg_match('/^[0-9]+$/D', $_GET['id'])) {
    $id = $_GET['id'];
    }
    $res = pq_query('SELECT * FROM product WHERE ID = '.$id);

    は、せめて $id を初期化しておいてください。(もしくは、if ブロックの中で query発行をするか。)

    Commented : #1  通りすがり (2007/06/10, 07:15)

コメントの記入