[解決済み] PDOのプリペアドステートメントは、SQLインジェクションを防ぐのに十分ですか?
質問
例えば、こんなコードがあったとします。
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDOのドキュメントにはこう書かれています。
プリペアド・ステートメントのパラメータは引用符で囲む必要はありません。
SQLインジェクションを避けるために必要なことは本当にこれだけですか? 本当に簡単なことなのでしょうか?
違いがあればMySQLを想定してください。 また、私は本当にSQLインジェクションに対するプリペアドステートメントの使用についてだけ興味があります。 この文脈では、XSSやその他の脆弱性の可能性は気にしません。
どのように解決するのか?
簡単に言うと 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));
特定の状況下では、それは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の\
すなわち0x5c
の前に'
という文字があります。ですから、最終的には0xbf5c27
で、これはgbk
は2文字の並びです。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()
はバックスラッシュを挿入し、フリーハンギングである'
という文字が、quot;escape"されたコンテンツに含まれているのです。実際、もし私たちが$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 がネイティブに準備できないステートメントをエミュレートするために使用されます。 リストアップ のマニュアルに記載されていますが、適切なサーバーのバージョンを選択するように注意してください)。
正しい修正方法
ここで問題なのは、C言語のAPIの
mysql_set_charset()
ではなく
SET NAMES
. もしそうであれば、2006年以降のMySQLのリリースを使用していれば問題ないでしょう。
それ以前のMySQLのリリースを使用している場合は
バグ
で
mysql_real_escape_string()
ペイロードに含まれるような無効なマルチバイト文字が、エスケープのためにシングルバイトとして扱われることを意味します。
クライアントが接続のエンコードを正しく通知していたとしても
そのため、この攻撃はまだ成功しています。 このバグはMySQLで修正されました。
4.1.20
,
5.0.22
および
5.1.11
.
しかし、最悪なのは
PDO
の C API を公開しませんでした。
mysql_set_charset()
5.3.6まで、それ以前のバージョンでは
できない
この攻撃は、可能な限りすべてのコマンドで防ぐことができます。
として公開されるようになりました。
DSNパラメータ
を使用する必要があります。
ではなく
SET NAMES
...
救いの手
冒頭で述べたように、この攻撃が機能するためには、データベース接続が脆弱な文字セットでエンコードされている必要があります。
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など)を使用する。 AND PDO の DSN charset パラメータ (PHP ≥ 5.3.6 の場合)
または
-
接続のエンコーディングに脆弱な文字セットを使用しない(あなたが使用するのは
utf8
/latin1
/ascii
/ など)
または
-
有効にする
NO_BACKSLASH_ESCAPES
SQLモード
100%安全です。
そうでなければ、あなたは無防備です PDO Prepared Statements を使っているのに・・・。
追記
将来のバージョンのPHPのために、準備のエミュレートをしないようにデフォルトを変更するパッチを少しずつ作っています。私が遭遇している問題は、それを行うと多くのテストが壊れるということです。一つの問題は、エミュレートされたprepareは実行時にのみ構文エラーを投げますが、本当のprepareはprepare時にエラーを投げます。そのため、問題を引き起こす可能性があります(そして、テストが失敗する理由の一部でもあります)。
関連
-
[解決済み] [Solved] Fatal error: メンバ関数prepare()のNULLでの呼び出し
-
[解決済み】XAMPPのphpMyAdminで「設定にあるcontroluserの接続に失敗しました。
-
[解決済み】XAMPPエラー: www.example.com:443:0 サーバー証明書に、サーバー名と一致するIDが含まれていません。
-
[解決済み】PHPからPythonスクリプトを実行する
-
[解決済み】未定義の関数mysql_query()をLoginで呼び出す【重複
-
[解決済み] PHP - ストリームを開くのに失敗しました : そのようなファイルまたはディレクトリがありません。
-
phpのAllowed memory size of 134217728 bytes枯渇問題の解決法
-
[解決済み] PHPでSQLインジェクションを防ぐにはどうしたらいいですか?
-
[解決済み] mysql_real_escape_string() を回避する SQL インジェクション
-
[解決済み】XKCDコミック「Bobby Tables」のSQLインジェクションはどのように動作するのでしょうか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】pdo - 非オブジェクトのメンバー関数prepare()への呼び出し【重複】。
-
[解決済み】foreach()に与えられた引数が無効です。)
-
[解決済み】既に開始されているPHPセッション【重複あり
-
[解決済み】Netbeans 7.4 for PHPで「スーパーグローバルな$_POST配列に直接アクセスしないでください」という警告が発生する。
-
[解決済み] PHP - ストリームを開くのに失敗しました : そのようなファイルまたはディレクトリがありません。
-
phpのAllowed memory size of 134217728 bytes枯渇問題の解決法
-
[解決済み】Wordpressの子テーマのstyle.cssが効かない。
-
[解決済み] SSLエラー SSL3_GET_SERVER_CERTIFICATE:証明書の検証に失敗しました。
-
[解決済み] PHP 未定義関数への呼び出し
-
[解決済み] 致命的なエラーです。mysqli_result 型のオブジェクトを使用できません [終了] 。