PDOの準備済みステートメントはSQLインジェクションを防ぐのに十分ですか?


660

私がこのようなコードを持っているとしましょう:

$dbh = new PDO("blahblah");

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

PDOドキュメントは言う:

準備済みステートメントのパラメーターは引用符で囲む必要はありません。ドライバーが処理します。

SQLインジェクションを回避するために本当に必要なことはそれだけですか?本当にそんなに簡単ですか?

それが違いを生む場合、MySQLを想定できます。また、SQLインジェクションに対するプリペアドステートメントの使用についてのみ興味があります。このコンテキストでは、XSSやその他の考えられる脆弱性については気にしません。


5
より良いアプローチ第七数の回答stackoverflow.com/questions/134099/...
NullPoiиteя

回答:


807

短い答えは「いいえ」です。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つのなどのエンコーディングがありますbig5cp932gb2312gbksjisgbkここで選択します。

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

  2. ペイロード

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

    呼び出しaddslashes()た場合、文字の前にASCII \ieを挿入するため、このペイロードを選択しました。我々が羽目になるだろうので、その中には、2つの文字列です:0x5c'0xbf5c27gbk0xbf5c続きます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()バックスラッシュ挿入'、「エスケープされた」コンテンツにフリーハンギング文字が含まれます。私たちが見にした場合、実際には、$var中にgbk文字セット、我々は参照してくださいね。

    縗 'OR 1 = 1 / *

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

  4. クエリ

    この部分は単なる形式ですが、ここにレンダリングされたクエリがあります:

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

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

簡単な修正

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

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

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

正しい修正

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

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

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

救いの恵み

最初に述べたように、この攻撃が機能するには、脆弱な文字セットを使用してデータベース接続をエンコードする必要があります。 utf8mb4脆弱ではなくすべての Unicode文字をサポートできます。そのため、代わりにそれを使用することを選択できますが、MySQL 5.5.3以降でのみ使用可能です。もう1つの方法はですutf8。これも脆弱ではなく、Unicode Basic Multilingual Plane全体をサポートできます。ます。

または、NO_BACKSLASH_ESCAPESSQLモードを有効にすることもできます。これにより、(特に)の操作が変更されmysql_real_escape_string()ます。このモードを有効に0x27すると、0x2727ではなくに置き換えられる0x5c27ため、エスケーププロセスでは、以前は存在しなかった(つまり、まだ存在する)脆弱なエンコーディングで有効な文字を作成できないため、サーバーは文字列を無効として拒否します。 。ただし、@ eggyalの回答を参照してください0xbf270xbf27この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_ESCAPESSQLモードを有効にする

あなたは100%安全です。

そうしないと、PDOの準備済みステートメントを使用している場合でも、脆弱になります...

補遺

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


47
これは私が見つけた最良の回答です。参照用のリンクを提供できますか?
StaticVariable 2012

1
@nicogawenda:それは別のバグでした。5.0.22より前のバージョンでmysql_real_escape_stringは、接続がBIG5 / GBKに正しく設定されている場合は正しく処理されませんでした。したがって、実際mysql_set_charset()にはmysql <5.0.22を呼び出すことでも、このバグに対して脆弱になります。いいえ、この投稿は引き続き5.0.22に適用されます(mysql_real_escape_stringはからの呼び出しに対してのみ文字セットであるためmysql_set_charset()、この投稿がバイパスについて話しているものです)...
ircmaxell

1
@progfa可能かどうかに関係なく、ユーザーデータを使用して何かを行う前に、サーバーでの入力を常に検証する必要があります。
Tek 14

2
NO_BACKSLASH_ESCAPES新しい脆弱性が導入される可能性があることに注意してください:stackoverflow.com/a/23277864/1014813
lepix

2
@slevin "OR 1 = 1"は、必要なもののプレースホルダーです。はい、名前で値を検索していますが、「OR 1 = 1」の部分が「UNION SELECT * FROM users」であったと想像してください。これでクエリを制御できるので、乱用される可能性があります...
ircmaxell '25年

515

準備されたステートメント/パラメーター化されたクエリは、通常、そのステートメントの1次オーダ注入を防ぐのに十分です*。チェックされていない動的SQLをアプリケーションの他の場所で使用した場合でも、2次に対して脆弱です。インジェクションです。

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

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

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

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

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


* MySql / PHPは、(大丈夫ですが)ワイド文字が含まれる場合のパラメーターの処理については馬鹿げていることが判明しました。また、他の投票数の多い回答で概説されている、パラメーター化されたインジェクションをすり抜けることができるまれなケースがまだあります。クエリ。


6
それは面白い。1次と2次の違いは知りませんでした。2次がどのように機能するかについてもう少し詳しく説明できますか?
Mark Biek

193
すべてのクエリがパラメーター化されている場合、2次注入からも保護されます。一次注入は、ユーザーデータが信頼できないことを忘れています。2次注入は、データベースデータが信頼できないことを忘れています(元々はユーザーからのものであるため)。
cjm 2008

6
ありがとうcjm。この記事は、2次インジェクションの説明にも役立ちました: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek

49
ああ、そうです。しかし、3次注入についてはどうでしょう。それらに注意する必要があります。
troelskn

81
開発者が信頼できないデータのソースである必要がある
@troelskn

45

いいえ、常にそうとは限りません。

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

$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']) );

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

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

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


11
ここに何か不足していますか?SQLを文字列のように扱うことを避けるために準備されたステートメントの全体のポイントではありませんか?$ dbh-> prepare( 'SELECT * FROM:tableToUse where username =:username');のようなものではありません あなたの問題を回避しますか?
Rob Forrest

4
@RobForrestはい、あなたは欠落しています:)。バインドしたデータは、DDL(データ定義言語)でのみ機能します。あなたはそれらの引用符と適切なエスケープが必要です。クエリの他の部分に引用符を付けると、高い確率で壊れます。たとえば、バックスティックの有無にかかわらず、SELECT * FROM 'table'間違っている可能性がありますSELECT * FROM `table`。次に、ユーザーがORDER BY DESCどこDESCから来たのかなどのいくつかのことを単純にエスケープすることはできません。したがって、実際的なシナリオはかなり無制限です。
タワー

8
6人が準備されたステートメントの明らかに間違った使用を提案するコメントに賛成票を投じることができるだろうかと思います。彼らが一度試してさえいれば、テーブル名の代わりに名前付きパラメーターを使用しても機能しないことがすぐにわかりました。
フェリックス・ガニオン-グルニエ

ここでは、PDOについて学習したい場合の優れたチュートリアルを示します。a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha

11
使用するテーブルを選択するために、クエリ文字列/ POSTボディを使用しないでください。モデルがない場合は、少なくともを使用しswitchてテーブル名を派生させます。
ZiggyTheHamster 14

29

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

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

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


10
ではない「決して」誤解を招くことのポイントにそれを誇張、。準備されたステートメントを誤って使用している場合、それらをまったく使用しないよりもはるかに良いことではありません。(もちろん、そこにユーザー入力が注入された「準備されたステートメント」は目的を達成しません...しかし、実際にそれが行われたのを見てきました。そして、準備されたステートメントは識別子(テーブル名など)をパラメーターとして処理できません。)追加それに加えて、一部のPDOドライバーは準備されたステートメントをエミュレートし、それらを誤って(たとえば、SQLを半端的に解析することによって)実行する余地があります。ショートバージョン:そんなに簡単だと思い込まないでください。
cHao

29

いいえ、これだけでは不十分です(特定の場合)。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()、正しいエンコーディングとクォーティングスタイルでもう一度使用します。


14

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


1

たとえhtmlまたはjsチェックを使用してSQLインジェクションフロントエンドを防止する場合でも、フロントエンドチェックは「バイパス可能」であることを考慮する必要があります。

jsを無効にしたり、フロントエンド開発ツール(最近、firefoxまたはchromeに組み込まれている)を使用してパターンを編集したりできます。

したがって、SQLインジェクションを防ぐために、コントローラ内の入力日付バックエンドをサニタイズするのが適切です。

GETおよびINPUT値をサニタイズするために、filter_input()ネイティブPHP関数を使用することをお勧めします。

セキュリティを優先したい場合は、賢明なデータベースクエリのために、正規表現を使用してデータ形式を検証することをお勧めします。この場合、preg_match()が役立ちます!しかし、注意してください!正規表現エンジンはそれほど軽量ではありません。必要な場合にのみ使用してください。そうしないと、アプリケーションのパフォーマンスが低下します。

セキュリティにはコストがかかりますが、パフォーマンスを無駄にしないでください!

簡単な例:

GETから受け取った値が99未満かどうかを再確認する場合if(!preg_match( '/ [0-9] {1,2} /')){...}の方が重い

if (isset($value) && intval($value)) <99) {...}

したがって、最終的な答えは次のとおりです。「いいえ!PDOの準備済みステートメントは、あらゆる種類のSQLインジェクションを妨げるものではありません」; 予期しない値を防止するのではなく、予期しない連結を防止します


5
SQLインジェクションを他の何かと混同しているため、回答がまったく無関係になります
常識
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.