web-dev-qa-db-ja.com

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や他の考えられる脆弱性については気にしません。

610
Mark Biek

簡単な答えは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 and ASCII \すなわち0x5c。結局のところ、MySQL 5.6ではデフォルトで、big5cp932gb2312gbkおよびsjisという5つのエンコーディングがサポートされています。ここでgbkを選択します。

    ここで、SET NAMESの使用に注意することが非常に重要です。これにより、文字セットサーバー上が設定されます。別の方法もありますが、もうすぐそこに着きます。

  2. ペイロード

    このインジェクションに使用するペイロードは、バイトシーケンス0xbf27で始まります。 gbkでは、これは無効なマルチバイト文字です。 latin1では、文字列¿'です。 latin1andgbkでは、0x27自体がリテラル'文字であることに注意してください。

    このペイロードを選択したのは、addslashes()を呼び出すと、\文字の前にASCII 0x5c、つまり'を挿入するためです。そのため、0xbf5c27を作成しました。これは、gbkでは2文字のシーケンスです:0xbf5cの後に0x27が続きます。または、言い換えると、 valid 文字の後にエスケープされていない'が続きます。ただし、addslashes()は使用していません。次のステップに進みます...

  3. $ stmt-> execute()

    ここで重要なことは、PDOがデフォルトでNOT真の準備済みステートメントを実行することです。それらをエミュレートします(MySQL用)。そのため、PDOはクエリ文字列を内部で構築し、バインドされた各文字列値でmysql_real_escape_string()(MySQL C API関数)を呼び出します。

    mysql_real_escape_string()へのC API呼び出しは、接続文字セットを知っているという点でaddslashes()と異なります。そのため、サーバーが予期している文字セットに対して適切にエスケープを実行できます。ただし、これまでクライアントは、接続にlatin1を使用していると考えています。 server gbkを使用していると伝えましたが、 client はまだlatin1であると考えています。

    したがって、mysql_real_escape_string()を呼び出すとバック​​スラッシュが挿入され、「エスケープされた」コンテンツにフリーハングの'文字が含まれます。実際、gbk文字セットの$varを見ると、次のように表示されます。

    縗 'OR 1 = 1/*

    これがまさに攻撃に必要なものです。

  4. クエリ

    この部分は単なる形式ですが、レンダリングされたクエリは次のとおりです。

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

おめでとうございます、PDO Prepared Statementsを使用してプログラムを攻撃しました...

簡単な修正

さて、エミュレートされた準備済みステートメントを無効にすることでこれを防ぐことができることに注意してください:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

これは通常の結果、準備されたステートメント(つまり、クエリとは別のパケットで送信されるデータ)になります。ただし、PDOは黙って フォールバック MySQLがネイティブに準備できないステートメントをエミュレートします:マニュアルでは listed ですが、適切なものを選択するように注意してくださいサーバーのバージョン)。

正しい修正

ここでの問題は、SET NAMESの代わりにC APIのmysql_set_charset()を呼び出さなかったことです。 2006年以降、MySQLリリースを使用していれば問題ありません。

以前のMySQLリリースを使用している場合、mysql_real_escape_string()の-​​ bug は、ペイロード内の文字などの無効なマルチバイト文字がエスケープの目的でシングルバイトとして扱われることを意味しましたクライアントに接続エンコーディングが正しく通知されていたとしても、この攻撃は成功します。このバグはMySQLで修正されました 4.1.25.0.22 および 5.1.11

しかし、最悪の部分は、PDOが5.3.6までmysql_set_charset()のC APIを公開していなかったため、以前のバージョンではcannot考えられるすべてのコマンドに対してこの攻撃を防ぎました。現在は DSNパラメーター として公開されており、これを使用する必要があります代わりにSET NAMES...

救いの恵み

最初に述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。 utf8mb4脆弱性がありませんであり、 every Unicode文字をサポートできます。しかし、MySQL 5.5.3からしか利用できません。代替手段は utf8 です。これも脆弱性がなくであり、Unicode全体をサポートできます Basic Multilingual Plane

または、 NO_BACKSLASH_ESCAPES SQLモードを有効にすることもできます。このモードは、(特に)mysql_real_escape_string()の操作を変更します。このモードを有効にすると、0x270x2727ではなく0x5c27に置き換えられるため、エスケーププロセス cannot は、以前は存在しなかった脆弱なエンコーディングで有効な文字を作成します(つまり0xbf27はまだ0xbf27など)-サーバーは文字列を無効として拒否します。ただし、このSQLモードの使用から生じる可能性のある別の脆弱性については、 @ eggyalの回答 を参照してください(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 charsetパラメーター(PHP≥5.3.6内)

または

  • 接続エンコードに脆弱な文字セットを使用しないでください(utf8/latin1/ascii /などのみを使用します)

または

  • NO_BACKSLASH_ESCAPES SQLモードを有効にする

あなたは100%安全です。

そうでなければ、脆弱ですPDO Prepared Statements ...を使用していても...

補遺

PHPの将来のバージョンの準備をエミュレートしないようにデフォルトを変更するパッチの作業をゆっくりと進めています。私が直面している問題は、それを行うと多くのテストが壊れることです。 1つの問題は、エミュレートされた準備は実行時に構文エラーをスローするだけですが、真の準備は準備時にエラーをスローすることです。そのため、問題が発生する可能性があります(テストが中断する理由の一部です)。

764
ircmaxell

通常、準備されたステートメント/パラメーター化されたクエリは、そのステートメントへの1st orderインジェクションを防ぐのに十分です。*。アプリケーションのどこかでチェックされていない動的SQLを使用する場合、まだ2nd orderインジェクションに対して脆弱です。

2次インジェクションとは、クエリに含まれる前にデータベースでデータが1回循環されることを意味し、引き離すのがはるかに困難です。知る限りでは、攻撃者は通常、ソーシャルエンジニアリングを行う方が簡単なので、実際に設計された2次攻撃はほとんど見られませんが、余分な'文字などが原因で2次バグが発生することがあります。

後でクエリでリテラルとして使用されるデータベースに値を保存できる場合、2次インジェクション攻撃を実行できます。例として、Webサイトでアカウントを作成するときに、新しいユーザー名として次の情報を入力するとします(この質問にはMySQL DBを想定しています)。

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

ユーザー名に他の制限がない場合、準備されたステートメントは、挿入時に上記の埋め込みクエリが実行されないことを確認し、値をデータベースに正しく格納します。ただし、後でアプリケーションがデータベースからユーザー名を取得し、文字列連結を使用してその値を新しいクエリに含めると想像してください。他の人のパスワードが表示される場合があります。 usersテーブルの最初のいくつかの名前は管理者である傾向があるので、あなたはちょうど農場を譲ったかもしれません。 (注意:これは、パスワードをプレーンテキストで保存しないもう1つの理由です!)

準備されたステートメントは単一のクエリには十分であることがわかりますが、それだけでアプリケーション全体のSQLインジェクション攻撃から保護するにはnotで十分です、アプリケーション内のデータベースへのすべてのアクセスで安全なコードを使用することを強制するメカニズムがないためです。ただし、コードレビューや静的分析などのプラクティス、または動的SQLを制限するORM、データレイヤー、またはサービスレイヤーの使用など、優れたアプリケーション設計の一部として使用されます— 準備されたステートメントare SQLインジェクション問題を解決するための主要なツールデータアクセスをプログラムの他の部分から分離するなど、適切なアプリケーション設計原則に従うと、すべてのクエリがパラメーター化を正しく使用していることを容易に実施または監査できます。この場合、SQLインジェクション(1次と2次の両方)は完全に防止されます。


*MySql/PHPは、ワイド文字が含まれる場合のパラメーターの処理について(とんでもない)ただ馬鹿げているだけであり、rareのケースが 他の非常に投票された答えhere インジェクションがパラメーター化されたクエリをすり抜けることができます。

507
Joel Coehoorn

いいえ、彼らは必ずしもそうではありません。

ユーザー入力をクエリ自体に含めることができるかどうかによって異なります。例えば:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

ユーザー入力はデータとしてではなく識別子として使用されるため、この例ではSQLインジェクションに対して脆弱であり、プリペアドステートメントを使用することは機能しません。ここで正しい答えは、次のようなある種のフィルタリング/検証を使うことです。

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

注意:PDLを使用してDDL(データ定義言語)の外側にあるデータをバインドすることはできません。つまり、これは機能しません。

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

DESCASCデータではないため、上記が機能しないのはその理由です。 PDOはデータに対してのみエスケープできます。次に、'を引用符で囲むことすらできません。ユーザーが選択した並べ替えを許可する唯一の方法は、手動でフィルター処理し、それがDESCまたはASCのいずれかであることを確認することです。

40
Tower

はい、それで十分です。インジェクション型攻撃が機能する方法は、何らかの方法でインタプリタ(データベース)に何かを評価させることです。これは、コードとデータを同じ媒体に混在させた場合にのみ可能です(たとえば、クエリを文字列として作成した場合)。

パラメータ化されたクエリは、コードとデータを別々に送信することで機能するので、決してその穴を見つけることはできません。

あなたはまだ他のインジェクションタイプの攻撃に対して脆弱である可能性があります。たとえば、HTMLページのデータを使用していると、XSSタイプの攻撃を受ける可能性があります。

26
troelskn

いいえ、これだけでは不十分です(特定のケースでは)。データベースドライバとしてMySQLを使用する場合、デフォルトでPDOはエミュレートされた準備済みステートメントを使用します。 MySQLとPDOを使用するときは、エミュレートされたプリペアドステートメントを常に無効にする必要があります。

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

常に行うべきもう1つのことは、データベースの正しいエンコードを設定することです。

$dbh = new PDO('mysql:dbname=dbtest;Host=127.0.0.1;charset=utf8', 'user', 'pass');

この関連質問も参照してください。 PHPでSQLインジェクションを防ぐにはどうすればよいですか?

また、データを表示するときに注意する必要があるのは、データベースに関するものだけです。例えば。正しいエンコーディングと引用符のスタイルでhtmlspecialchars()をもう一度使用する。

25
PeeHaa

個人的には、ユーザー入力を信頼することはできないため、常に最初に何らかの形式のサニタイズを実行しますが、プレースホルダー/パラメーターバインディングを使用すると、入力データは個別にsqlステートメントとサーバーに送信され、結合されます。ここで重要なことは、これが提供されたデータを特定のタイプと特定の用途に結び付け、SQLステートメントのロジックを変更する機会を排除することです。

10
JimmyJ