711

次のようなコードがあるとします。

$dbh = new PDO("blahblah");

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

PDOのドキュメントには次のように書かれています:

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

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

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

4

7 に答える 7

889

短い答えは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 ie のようにエンコード'され、最終バイトが ASCII ie0x27 ある文字を持つことの両方が必要です。結局のところ、MySQL 5.6 ではデフォルトで、 、、 の5つのエンコーディングがサポートされています。ここで選択します。\0x5cbig5cp932gb2312gbksjisgbk

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

  2. ペイロード

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

    このペイロードを選択したのは、それを呼び出した場合、文字の前にaddslashes()ASCII \ieを挿入するためです。これは、2 文字のシーケンスであり、その後に . が続きます。つまり、有効な文字の後にエスケープされていない. しかし、私たちは使用していません。それでは次のステップへ…0x5c'0xbf5c27gbk0xbf5c0x27'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
    

おめでとうございます。PDO プリペアド ステートメントを使用してプログラムへの攻撃に成功しました...

簡単な修正

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

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

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

正しい修正

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

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

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

セービング・グレイス

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

または、SQL モードを有効にすることもできNO_BACKSLASH_ESCAPESます。これにより、(とりわけ) の動作が変更されmysql_real_escape_string()ます。このモードを有効に0x27すると、 は not に置き換えられるため、エスケープ プロセスは、以前は存在しなかった脆弱なエンコーディングで有効な文字を作成できません (つまり0x2727まだ存在するなど)。そのため、サーバーは文字列を無効として引き続き拒否します。 . ただし、この SQL モードを使用することで発生する可能性のある別の脆弱性については、@eggyal の回答を参照してください (ただし、PDO ではありません)。0x5c270xbf270xbf27

安全な例

次の例は安全です。

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_ESCAPESSQL モードを有効にする

あなたは 100% 安全です。

そうしないと、PDO プリペアド ステートメントを使用していても脆弱になります...

補遺

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

于 2012-08-30T17:22:04.327 に答える
521

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

2次インジェクションは、データがクエリに含まれる前にデータベースを1回循環していることを意味し、実行するのがはるかに困難です. '知る限り、実際に設計された 2 次攻撃を目にすることはほとんどありません。通常、攻撃者はソーシャル エンジニアリングを利用して侵入する方が簡単ですが、無害なキャラクターなどが追加されたために 2 次バグが発生することがあります。

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

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

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

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


* MySql/PHP は、ワイド文字が含まれる場合のパラメーターの処理について (わかりました) 愚かであることが判明しました。また、ここでの他の高投票数の回答で概説されているまれなケースがあり、パラメーター化されたパラメーターを介して注入が行われる可能性があります。クエリ。

于 2008-09-25T15:50:51.960 に答える
46

いいえ、いつもではありません。

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

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

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

于 2010-04-21T09:00:06.190 に答える
29

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

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

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

于 2008-09-25T15:55:46.533 に答える
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()、正しいエンコーディングと引用スタイルで再度使用します。

于 2012-08-30T17:00:15.760 に答える
14

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

于 2008-09-25T15:50:43.770 に答える