PDO プリペアド ステートメントは SQL インジェクションを防ぐのに十分ですか?
-
02-07-2019 - |
質問
次のようなコードがあるとします。
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDO のドキュメントには次のように書かれています。
準備されたステートメントのパラメーターを引用符で囲む必要はありません。ドライバーが代わりに対応してくれます。
SQL インジェクションを回避するために必要な作業は本当にこれだけですか?本当にそんなに簡単ですか?
違いがある場合は、MySQL を想定してください。また、私が実際に興味があるのは、SQL インジェクションに対するプリペアド ステートメントの使用についてだけです。この文脈では、XSS やその他の潜在的な脆弱性については気にしません。
解決
短い答えは次のとおりです いいえ, PDO prepare は、考えられるすべての 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 のように、つまり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()
バックスラッシュを挿入すると、フリーハングが得られます'
「脱出」コンテンツのキャラクターです!実際に見てみると、$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
は 脆弱ではない それでもサポートできる 毎 ユニコード文字:したがって、代わりにそれを使用することもできますが、これは 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 など) を使用します。 そして PDO の DSN charset パラメータ (PHP ≥ 5.3.6)
または
- 接続エンコーディングに脆弱な文字セットを使用しないでください(使用するのは
utf8
/latin1
/ascii
/など)
または
- 有効にする
NO_BACKSLASH_ESCAPES
SQLモード
100%安全です。
そうでなければ、あなたは脆弱になります PDO プリペアドステートメントを使用しているにもかかわらず...
補遺
私は、PHP の将来のバージョンに備えて、デフォルトをエミュレートしないように変更するパッチにゆっくりと取り組んできました。私が直面している問題は、これを行うと多くのテストが中断されることです。1 つの問題は、エミュレートされた prepare は実行時に構文エラーのみをスローしますが、真の prepare は準備時にエラーをスローすることです。そのため、それが問題を引き起こす可能性があります (これがテストが失敗する理由の 1 つです)。
他のヒント
通常、プリペアドステートメント/パラメータ化されたクエリは、これを防ぐのに十分です。 1次 そのステートメントに対する注入*. 。アプリケーション内の他の場所でチェックされていない動的 SQL を使用した場合でも、次のような脆弱性が残ります。 2次注文 注射。
2 次注入とは、データがクエリに含まれる前にデータベースを 1 回循環することを意味し、それを実行するのは非常に困難です。私の知る限り、攻撃者がソーシャルエンジニアリングを行って侵入するのは通常簡単であるため、実際に設計された二次攻撃を目にすることはほとんどありませんが、余分な害のないもののために二次バグが発生することがあります。 '
キャラクターまたはそれに類似したもの。
値をデータベースに保存し、後でクエリでリテラルとして使用できる場合、2 次インジェクション攻撃を実行できます。例として、Web サイトでアカウントを作成するときに、新しいユーザー名として次の情報を入力するとします (この質問では MySQL DB を想定しています)。
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
ユーザー名に他の制限がない場合でも、準備されたステートメントは、挿入時に上記の埋め込みクエリが実行されないことを確認し、値をデータベースに正しく保存します。ただし、後でアプリケーションがデータベースからユーザー名を取得し、文字列連結を使用してその値を新しいクエリに含めると想像してください。他人のパスワードを見られる可能性があります。ユーザー テーブルの最初の数人の名前は管理者である傾向があるため、ファームを譲渡したばかりである可能性もあります。(次の点にも注意してください:これがパスワードを平文で保存すべきではないもう 1 つの理由です!)
したがって、準備されたステートメントは単一のクエリには十分ですが、それ自体では十分であることがわかります。 ない SQL インジェクション攻撃には、アプリケーション内のデータベースへのすべてのアクセスに安全なコードの使用を強制するメカニズムがないため、アプリケーション全体にわたる SQL インジェクション攻撃から保護するには十分です。ただし、優れたアプリケーション設計の一部として使用されます。これには、コード レビューや静的分析、または動的 SQL を制限する ORM、データ層、サービス層の使用などの実践が含まれる場合があります。 準備されたステートメント は SQL インジェクションの問題を解決するための主要なツール。 データ アクセスをプログラムの残りの部分から分離するなど、適切なアプリケーション設計原則に従えば、すべてのクエリでパラメーター化が正しく使用されていることを強制したり監査したりすることが簡単になります。この場合、SQL インジェクション (1 次と 2 次の両方) は完全に防止されます。
*MySql/PHP は、ワイド文字が含まれる場合のパラメーターの処理について (まあ、そうだったのですが) 単に愚かであることが判明しました。 レア で概説されているケース 他の投票の多かった回答はこちら これにより、インジェクションがパラメータ化されたクエリをすり抜けてしまう可能性があります。
いいえ、常にそうとは限りません。
それは、クエリ自体内にユーザー入力を配置できるかどうかによって異なります。例えば:
$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');
上記が機能しない理由は次のとおりです。 DESC
そして ASC
ではありません データ. 。PDO は次の目的でのみエスケープできます。 データ. 。第二に、置くことさえできません '
その周りの引用。ユーザーが選択した並べ替えを許可する唯一の方法は、手動でフィルタリングして、次のいずれかであることを確認することです。 DESC
または ASC
.
はい、十分です。インジェクションタイプの攻撃は、何らかの方法でインタプリタ (データベース) に、データであるべきものをコードであるかのように評価させることによって行われます。これは、同じ媒体内でコードとデータを混在させる場合にのみ可能です (例:クエリを文字列として構築する場合)。
パラメータ化されたクエリは、コードとデータを別々に送信することで機能します。 一度もない そこに穴を見つけることは可能です。
ただし、他のインジェクションタイプの攻撃に対しては依然として脆弱である可能性があります。たとえば、HTML ページでデータを使用すると、XSS タイプの攻撃を受ける可能性があります。
いいえ、これだけでは十分ではありません (特定の場合)。デフォルトでは、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()
正しいエンコードと引用スタイルでもう一度やり直してください。
ユーザー入力は決して信頼できないため、個人的には常に最初にデータに対して何らかの形式のサニテーションを実行しますが、プレースホルダー/パラメーターバインディングを使用する場合、入力されたデータは SQL ステートメントとは別にサーバーに送信され、その後一緒にバインドされます。ここで重要なのは、これにより、提供されたデータが特定のタイプおよび特定の用途にバインドされ、SQL ステートメントのロジックを変更する機会が排除されるということです。
html または js チェックを使用してフロントエンドでの SQL インジェクションを防止する場合でも、フロントエンド チェックが「バイパス可能」であることを考慮する必要があります。
フロントエンド開発ツール (最近では Firefox または Chrome に組み込まれています) を使用して、JS を無効にしたり、パターンを編集したりできます。
したがって、SQL インジェクションを防ぐには、コントローラー内の入力データ バックエンドをサニタイズするのが正しいでしょう。
GET 値と INPUT 値をサニタイズするには、filter_input() ネイティブ PHP 関数を使用することをお勧めします。
セキュリティを重視し、賢明なデータベース クエリを実行したい場合は、正規表現を使用してデータ形式を検証することをお勧めします。この場合には preg_match() が役立ちます。でも気をつけて!Regex エンジンはそれほど軽量ではありません。必要な場合にのみ使用してください。使用しないと、アプリケーションのパフォーマンスが低下します。
セキュリティにはコストがかかりますが、パフォーマンスを無駄にしないでください。
簡単な例:
GETから受信した値が数字であるかどうかを再確認する場合、99 if(!preg_match( '/[0-9] {1,2}/')){...}は重いです
if (isset($value) && intval($value)) <99) {...}
したがって、最終的な答えは次のようになります。"いいえ!PDO プリペアド ステートメントはあらゆる種類の SQL インジェクションを防ぐわけではありません。」予期しない値を防ぐのではなく、予期しない連結を防ぐだけです