短い答えはNOです。PDO 準備は、考えられるすべての SQL インジェクション攻撃からあなたを守るわけではありません。特定のあいまいなエッジケース用。
私はこの答えを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));
特定の状況では、複数の行が返されます。ここで何が起こっているのかを分析しましょう。
文字セットの選択
$pdo->query('SET NAMES gbk');
この攻撃が機能するには、サーバーが接続で期待するエンコーディングが、ASCII ie のようにエンコード'
され、最終バイトが ASCII ie0x27
である文字を持つことの両方が必要です。結局のところ、MySQL 5.6 ではデフォルトで、 、、 の5つのエンコーディングがサポートされています。ここで選択します。\
0x5c
big5
cp932
gb2312
gbk
sjis
gbk
ここで、ここでの使用に注意することが非常に重要SET NAMES
です。これにより、文字セットがサーバーに設定されます。それを行う別の方法がありますが、すぐにそこにたどり着きます。
ペイロード
このインジェクションに使用するペイロードは、バイト シーケンスで始まります0xbf27
。ではgbk
、これは無効なマルチバイト文字です。ではlatin1
、文字列¿'
です。latin1
inと gbk
,0x27
は、それ自体がリテラル文字であることに注意してください'
。
このペイロードを選択したのは、それを呼び出した場合、文字の前にaddslashes()
ASCII \
ieを挿入するためです。これは、2 文字のシーケンスであり、その後に . が続きます。つまり、有効な文字の後にエスケープされていない. しかし、私たちは使用していません。それでは次のステップへ…0x5c
'
0xbf5c27
gbk
0xbf5c
0x27
'
addslashes()
$stmt->execute()
ここで理解しておくべき重要なことは、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 プリペアド ステートメントを使用してプログラムへの攻撃に成功しました...
簡単な修正
ここで、エミュレートされた準備済みステートメントを無効にすることでこれを防ぐことができることに注意してください。
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
これは通常、真の準備済みステートメントになります (つまり、クエリとは別のパケットでデータが送信されます)。ただし、PDO は、MySQL がネイティブに準備できないステートメントをエミュレートするために静かにフォールバックすることに注意してください: 可能なものはマニュアルにリストされていますが、適切なサーバー バージョンを選択するように注意してください)。
正しい修正
mysql_set_charset()
ここでの問題は、 の代わりにC API を呼び出さなかったことですSET NAMES
。その場合、2006 年以降の MySQL リリースを使用していれば問題ありません。
以前の MySQL リリースを使用している場合、バグは、クライアントが接続エンコーディングを正しく通知されていたとしてもmysql_real_escape_string()
、ペイロード内のような無効なマルチバイト文字がエスケープ目的で単一バイトとして扱われることを意味していたため、この攻撃はそれでも成功します。このバグは、MySQL 4.1.20、5.0.22、および5.1.11で修正されました。
しかし最悪の部分は、5.3.6 までPDO
C API を公開しなかったことです。そのため、以前のバージョンでは、すべての可能なコマンドに対してこの攻撃を防ぐことはできません! これは、代わりに使用する必要があるDSN パラメータとして公開されるようになりました...mysql_set_charset()
SET NAMES
セービング・グレイス
冒頭で述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。 utf8mb4
は脆弱ではありませんが、すべてのUnicode 文字をサポートできます。そのため、代わりにそれを使用することを選択できますが、MySQL 5.5.3 以降でしか利用できません。代替手段は ですutf8
。これも脆弱ではなく、Unicode Basic Multilingual Plane全体をサポートできます。
または、SQL モードを有効にすることもできNO_BACKSLASH_ESCAPES
ます。これにより、(とりわけ) の動作が変更されmysql_real_escape_string()
ます。このモードを有効に0x27
すると、 は not に置き換えられるため、エスケープ プロセスは、以前は存在しなかった脆弱なエンコーディングで有効な文字を作成できません (つまり0x2727
、まだ存在するなど)。そのため、サーバーは文字列を無効として引き続き拒否します。 . ただし、この SQL モードを使用することで発生する可能性のある別の脆弱性については、@eggyal の回答を参照してください (ただし、PDO ではありません)。0x5c27
0xbf27
0xbf27
安全な例
次の例は安全です。
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 charset パラメーター (PHP ≥ 5.3.6) を使用します。
また
- 接続エンコーディングに脆弱な文字セットを使用しないでください (
utf8
/ latin1
/ ascii
/ などのみを使用します) 。
また
NO_BACKSLASH_ESCAPES
SQL モードを有効にする
あなたは 100% 安全です。
そうしないと、PDO プリペアド ステートメントを使用していても脆弱になります...
補遺
PHP の将来のバージョンに備えて、デフォルトをエミュレートしないように変更するパッチにゆっくりと取り組んでいます。私が直面している問題は、それを行うと多くのテストが中断することです。問題の 1 つは、エミュレートされた準備では実行時に構文エラーがスローされるだけですが、真の準備では準備時にエラーがスローされることです。そのため、問題が発生する可能性があります (テストがうまくいかない理由の一部です)。