1. ホーム
  2. php

[解決済み] PDOのプリペアドステートメントは、SQLインジェクションを防ぐのに十分ですか?

2022-03-14 12:43:11

質問

例えば、こんなコードがあったとします。

$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行以上を返します。ここで何が起こっているのか解剖してみましょう。

  1. キャラクタセットの選択

    $pdo->query('SET NAMES gbk');
    
    

    この攻撃を成功させるためには、サーバーが接続時に期待しているエンコーディングの両方が ' をASCIIのように、すなわち 0x27 を使用すると、最終バイトがASCIIである何らかの文字を持つことができます。 \ すなわち 0x5c . MySQL 5.6 では、デフォルトで 5 つのエンコーディングがサポートされていることがわかりました。 big5 , cp932 , gb2312 , gbksjis . ここでは gbk をここで紹介します。

    さて、ここで非常に重要なのが、「」の使い方です。 SET NAMES ここで これは、文字セット サーバー上 . 別の方法もありますが、それはまた今度。

  2. ペイロード

    このインジェクションに使用するペイロードは、以下のバイト列から始まります。 0xbf27 . で gbk では、これは無効なマルチバイト文字です。 latin1 という文字列です。 ¿' . ただし latin1 gbk , 0x27 は、それ自体ではリテラル ' 文字になります。

    このペイロードを選択した理由は、仮に addslashes() を挿入し、その上にASCIIの \ すなわち 0x5c の前に ' という文字があります。ですから、最終的には 0xbf5c27 で、これは gbk は2文字の並びです。 0xbf5c に続いて 0x27 . あるいは、言い換えれば 有効 文字に続き、エスケープされていない ' . しかし、私たちが使っているのは addslashes() . では、次のステップへ...

  3. $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 /*。
    

    これはまさに、この攻撃が要求していることです。

  4. クエリ

    この部分は形式的なものですが、レンダリングされたクエリーは以下の通りです。

    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時にエラーを投げます。そのため、問題を引き起こす可能性があります(そして、テストが失敗する理由の一部でもあります)。