次のようなコードがあるとします。
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDO ドキュメントには次のように書かれています:
準備されたステートメントのパラメータは引用符で囲む必要はなく、ドライバーがそれを処理します。
SQL インジェクションを回避するために必要なことは本当にこれだけですか? 本当にそんなに簡単なのでしょうか?
違いがある場合は、MySQL を前提とすることができます。また、SQL インジェクションに対する準備済みステートメントの使用についてのみ興味があります。このコンテキストでは、XSS やその他の潜在的な脆弱性については気にしません。
ベストアンサー1
簡単に答えると、はい、PDO 準備は適切に使用すれば十分に安全です。
私は適応しているこの答えPDOについてお話しましょう...
長い答えはそう簡単ではありません。それは攻撃に基づいていますここで実証。
攻撃
それでは、まずは攻撃の様子から見ていきましょう...
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
状況によっては、1 行以上が返されることがあります。ここで何が起こっているのか分析してみましょう。
文字セットの選択
$pdo->query('SET NAMES gbk');
この攻撃が成功するには、サーバーが接続時に想定しているエンコーディングが
'
ASCII でエンコードされていること (つまり )0x27
、および最終バイトが ASCII である文字が含まれていること (つまり )\
が必要0x5c
です。MySQL 5.6 では、デフォルトで 、 、 、 の 5 つのエンコーディングがサポートされています。big5
ここcp932
でgb2312
はgbk
選択sjis
しますgbk
。ここで、 の使用に注意することが非常に重要です。これにより、サーバー上の
SET NAMES
文字セットが設定されます。これを行う別の方法もありますが、すぐに説明します。ペイロード
このインジェクションに使用するペイロードは、バイト シーケンス で始まります
0xbf27
。 ではgbk
、これは無効なマルチバイト文字です。 ではlatin1
、これは文字列 です¿'
。latin1
およびgbk
では、0x27
それ自体はリテラル'
文字であることに注意してください。このペイロードを選択したのは、これを呼び出すと、文字の前に
addslashes()
ASCII が挿入されるから\
です。その結果、 になり、これは2 つの文字シーケンス の後に が続きます。つまり、有効な文字の後にエスケープされていない が続きます。ただし、 は使用しません。それでは、次のステップに進みます...0x5c
'
0xbf5c27
gbk
0xbf5c
0x27
'
addslashes()
$stmt->実行()
ここで理解しておくべき重要なことは、PDO はデフォルトでは実際に準備されたステートメントを実行しないということです。PDOはそれらをエミュレートします (MySQL 用)。したがって、PDO は内部的にクエリ文字列を構築し、
mysql_real_escape_string()
バインドされた各文字列値に対して (MySQL C API 関数を) 呼び出します。への C API 呼び出しは、接続文字セットを認識している点
mysql_real_escape_string()
で と異なりますaddslashes()
。そのため、サーバーが期待する文字セットに対して適切にエスケープを実行できます。ただし、この時点では、クライアントは接続にまだ を使用していると考えています。これは、他の方法を指示していないためです。サーバーにを使用しているlatin1
ことを伝えましたが、クライアントは依然としてであると考えています。gbk
latin1
したがって、 の呼び出しによって
mysql_real_escape_string()
バックスラッシュが挿入され、「エスケープされた」コンテンツに自由にぶら下がっている文字が存在します。実際、文字セットを'
見ると、次のようになります。$var
gbk
縗' OR 1=1 /*
まさにそれが攻撃に必要なことです。
クエリ
この部分は単なる形式的なものですが、レンダリングされたクエリは次のとおりです。
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
おめでとうございます。PDO Prepared Statements を使用してプログラムへの攻撃に成功しました...
簡単な解決策
ここで注目すべきは、エミュレートされた準備済みステートメントを無効にすることでこれを防ぐことができるということです。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
これは通常、真の準備されたステートメント(つまり、クエリとは別のパケットでデータが送信される)になります。ただし、PDOは暗黙的に後退するMySQLがネイティブに準備できないステートメントをエミュレートします。リストされているマニュアルに記載されていますが、適切なサーバー バージョンを選択するように注意してください。
正しい修正方法
ここでの問題は、SET NAMES
C APIの代わりにを使用していることですmysql_set_charset()
。そうでなければ、攻撃は成功しません。しかし、最悪なのは、PDOがmysql_set_charset()
5.3.6までC APIを公開していなかったため、以前のバージョンでは、あらゆるコマンドに対してこの攻撃を防ぐことができなかったことです。現在は、DSNパラメータ...の代わりに これを使用する必要があります。SET NAMES
これは、2006年以降のMySQLリリースを使用している場合に限ります。それ以前のMySQLリリースを使用している場合は、バグではmysql_real_escape_string()
、ペイロード内の無効なマルチバイト文字は、クライアントが接続エンコーディングを正しく通知されていたとしても、エスケープの目的で1バイトとして扱われ、この攻撃は成功していました。このバグはMySQLで修正されました。4.1.20、5.0.22そして5.1.11。
救いの恵み
冒頭で述べたように、この攻撃が機能するには、データベース接続が脆弱な文字セットを使用してエンコードされている必要があります。utf8mb4
脆弱性はなく、すべてのUnicode文字をサポートしているので、代わりにそれを使用することを選択できますが、MySQL 5.5.3以降でのみ利用可能です。代替案としては、utf8
これも脆弱性がなく、Unicode全体をサポートできる。基本的な多言語面。
あるいは、NO_BACKSLASH_ESCAPES
SQL モードは、(他の機能の中でも) の動作を変更しますmysql_real_escape_string()
。このモードを有効にすると、はではなく に0x27
置き換えられ、したがって、エスケープ処理では、脆弱なエンコーディングで以前に存在しなかった有効な文字を作成できません(つまり、はまだなど)。そのため、サーバーは文字列を無効として拒否します。ただし、0x2727
0x5c27
0xbf27
0xbf27
@eggyal の回答この SQL モードの使用によって発生する可能性がある別の脆弱性 (PDO 以外)。
安全な例
次の例は安全です:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
サーバーが期待しているためutf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
クライアントとサーバーが一致するように文字セットを適切に設定したためです。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
エミュレートされた準備済みステートメントをオフにしたためです。
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
文字セットを適切に設定したためです。
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
MySQLi は常に真の準備済みステートメントを実行するためです。
まとめ
もし、あんたが:
- 最新バージョンの MySQL (5.1 以降、5.5 以降、5.6 など)とPDO の DSN 文字セット パラメータ (PHP ≥ 5.3.6) を使用します。
または
- 接続エンコーディングに脆弱な文字セットを使用しないでください(
utf8
/latin1
/ascii
/ などのみを使用します)
または
NO_BACKSLASH_ESCAPES
SQLモードを有効にする
あなたは100%安全です。
そうしないと、 PDO Prepared Statements を使用していても脆弱になります...
補遺
私は、PHP の将来のバージョンで、prepare をエミュレートしないようにデフォルトを変更するパッチをゆっくりと作成しています。私が直面している問題は、それを行うと多くのテストが壊れてしまうことです。1 つの問題は、エミュレートされた prepare は実行時に構文エラーのみをスローしますが、実際の prepare は準備時にエラーをスローすることです。そのため、問題が発生する可能性があります (テストが失敗する理由の 1 つです)。