PHPでSQLインジェクションを防ぐにはどうすればよいですか?
次の例のように、ユーザー入力が変更なしでSQLクエリに挿入されると、アプリケーションはSQLインジェクションに対して脆弱になります。
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
これは、ユーザーがのようなものを入力できvalue'); DROP TABLE table;--
、クエリが次のようになるためです。
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
これを防ぐために何ができるでしょうか?
回答
プリペアドステートメントとパラメータ化されたクエリを使用します。これらは、パラメータとは別にデータベースサーバーに送信され、データベースサーバーによって解析されるSQLステートメントです。このように、攻撃者が悪意のあるSQLを挿入することは不可能です。
これを実現するには、基本的に2つのオプションがあります。
PDOの使用(サポートされているデータベースドライバーの場合):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
MySQLiの使用(MySQL用):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string' $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // Do something with $row }
あなたは、MySQL以外のデータベースに接続する場合は、あなたが(例えば、参照することができますことをドライバ固有の第二の選択肢がありpg_prepare()
とpg_execute()
PostgreSQLのは)。PDOは普遍的なオプションです。
接続を正しく設定する
PDO
MySQLデータベースへのアクセスにを使用する場合、実際に準備されたステートメントはデフォルトでは使用されないことに注意してください。これを修正するには、プリペアドステートメントのエミュレーションを無効にする必要があります。PDOを使用して接続を作成する例は次のとおりです。
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
上記の例では、エラーモードは厳密には必要ありませんが、追加することをお勧めします。このようにしてFatal Error
、問題が発生したときにスクリプトが停止することはありません。また、開発者は、nがsでcatch
あるエラーが発生する可能性があります。throw
PDOException
ただし、必須なのは最初のsetAttribute()
行です。これは、エミュレートされた準備済みステートメントを無効にし、実際の準備済みステートメントを使用するようにPDOに指示します。これにより、ステートメントと値がMySQLサーバーに送信される前にPHPによって解析されないようになります(攻撃者が悪意のあるSQLを挿入する機会がなくなります)。
charset
コンストラクターのオプションでを設定できますが、「古い」バージョンのPHP(5.3.6より前)はDSNのcharsetパラメーターを黙って無視することに注意することが重要です。
説明
渡すSQLステートメントprepare
は、データベースサーバーによって解析およびコンパイルされます。パラメータ(?
または:name
上記の例のような名前付きパラメータ)を指定することにより、フィルタリングする場所をデータベースエンジンに指示します。次に、を呼び出すexecute
と、プリペアドステートメントが指定したパラメータ値と組み合わされます。
ここで重要なことは、パラメーター値がSQL文字列ではなく、コンパイルされたステートメントと組み合わされることです。SQLインジェクションは、データベースに送信するSQLを作成するときに、スクリプトをだまして悪意のある文字列を含めることで機能します。したがって、実際のSQLをパラメーターとは別に送信することにより、意図しないものになってしまうリスクを制限できます。
プリペアドステートメントを使用するときに送信するパラメーターは、文字列として扱われます(ただし、データベースエンジンが最適化を行うため、パラメーターも数値になる場合があります)。上記の例では、$name
変数に'Sarah'; DELETE FROM employees
結果が含まれている場合、結果は単に文字列の検索になり、空のテーブルになっ"'Sarah'; DELETE FROM employees"
てしまうことはありません。
プリペアドステートメントを使用するもう1つの利点は、同じセッションで同じステートメントを何度も実行すると、解析とコンパイルが1回だけ行われるため、速度がいくらか向上することです。
ああ、そしてあなたが挿入のためにそれをする方法について尋ねたので、ここに例があります(PDOを使用して):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute([ 'column' => $unsafeValue ]);
プリペアドステートメントは動的クエリに使用できますか?
クエリパラメータにプリペアドステートメントを使用することはできますが、動的クエリ自体の構造をパラメータ化することはできず、特定のクエリ機能をパラメータ化することはできません。
これらの特定のシナリオでは、可能な値を制限するホワイトリストフィルターを使用するのが最善の方法です。
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
非推奨の警告: この回答のサンプルコード(質問のサンプルコードと同様)は
MySQL
、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPの拡張機能を使用しています。セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用してください。以下に概説する戦略は、自己責任で使用してください。(また、
mysql_real_escape_string()
PHP 7で削除されました。)
最新バージョンのPHPを使用しているmysql_real_escape_string
場合、以下に概説するオプションは使用できなくなります(ただしmysqli::escape_string
、最新の同等機能です)。最近のmysql_real_escape_string
オプションは、古いバージョンのPHPのレガシーコードにのみ意味があります。
の特殊文字をエスケープするunsafe_variable
か、パラメータ化されたクエリを使用するかの2つのオプションがあります。どちらもSQLインジェクションからあなたを守ります。パラメータ化されたクエリはより良い方法と考えられていますが、使用する前にPHPで新しいMySQL拡張機能に変更する必要があります。
最初に1つをエスケープする低衝撃ストリングについて説明します。
//Connect
$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
//Disconnect
mysql_real_escape_string
関数の詳細も参照してください。
パラメータ化されたクエリを使用するには、MySQL関数ではなくMySQLiを使用する必要があります。例を書き直すには、次のようなものが必要になります。
<?php
$mysqli = new mysqli("server", "username", "password", "database_name");
// TODO - Check that connection was successful.
$unsafe_variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// TODO check that $stmt creation succeeded
// "s" means the database expects a string
$stmt->bind_param("s", $unsafe_variable);
$stmt->execute();
$stmt->close();
$mysqli->close();
?>
あなたがそこに読みたいと思うであろう重要な機能はあるでしょうmysqli::prepare
。
また、他の人が示唆しているように、PDOのようなもので抽象化レイヤーをステップアップする方が便利で簡単だと思うかもしれません。
あなたが尋ねたケースはかなり単純なものであり、より複雑なケースはより複雑なアプローチを必要とするかもしれないことに注意してください。特に:
- ユーザー入力に基づいてSQLの構造を変更する場合、パラメーター化されたクエリは役に立ちません
mysql_real_escape_string
。また、必要なエスケープはでカバーされません。このような場合は、ユーザーの入力をホワイトリストに渡して、「安全な」値のみが許可されるようにすることをお勧めします。 - 条件でユーザー入力からの整数を使用して
mysql_real_escape_string
アプローチをとると、以下のコメントで多項式によって説明されている問題が発生します。整数は引用符で囲まれないため、このケースは扱いにくいため、ユーザー入力に数字のみが含まれていることを検証することで対処できます。 - 私が気付いていない他のケースがある可能性があります。これは、発生する可能性のあるより微妙な問題のいくつかに関する有用なリソースであることがわかるかもしれません。
ここでのすべての答えは、問題の一部のみをカバーしています。実際、SQLに動的に追加できる4つの異なるクエリ部分があります。
- 文字列
- 数
- 識別子
- 構文キーワード
そして、準備されたステートメントはそれらのうちの2つだけをカバーします。
ただし、演算子や識別子を追加して、クエリをさらに動的にする必要がある場合もあります。したがって、さまざまな保護手法が必要になります。
一般に、このような保護アプローチはホワイトリストに基づいています。
この場合、すべての動的パラメーターをスクリプトにハードコーディングし、そのセットから選択する必要があります。たとえば、動的な順序付けを行うには、次のようにします。
$orders = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically.
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe
プロセスを簡単にするために、すべてのジョブを1行で実行するホワイトリストヘルパー関数を作成しました。
$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe
識別子を保護する別の方法があります-エスケープしますが、より堅牢で明示的なアプローチとして、ホワイトリストに固執します。ただし、識別子が引用符で囲まれている限り、引用符をエスケープして安全にすることができます。たとえば、mysqlのデフォルトでは、引用符を2倍にしてエスケープする必要があります。他のDBMSの場合、エスケープルールは異なります。
それでも、(ようなSQL構文のキーワードに問題があるAND
、DESC
など)が、しかし、ホワイトリストは、この場合の唯一の方法と思われます。
したがって、一般的な推奨事項は次のように表現できます。
- SQLデータリテラルを表す変数(または、簡単に言うと、SQL文字列または数値)は、プリペアドステートメントを介して追加する必要があります。例外なく。
- SQLキーワード、テーブル名、フィールド名、演算子など、その他のクエリ部分は、ホワイトリストでフィルタリングする必要があります。
更新
SQLインジェクション保護に関するベストプラクティスについては一般的な合意がありますが、それでも多くの悪いプラクティスがあります。そして、それらのいくつかは、PHPユーザーの心に深く根付いています。たとえば、このページには(ほとんどの訪問者には見えませんが)80を超える削除された回答があります。これらはすべて、品質が悪いか、古くて悪い慣行を促進しているためにコミュニティによって削除されています。さらに悪いことに、悪い答えのいくつかは削除されず、むしろ繁栄しています。
たとえば、(1) は まだ(3) 多くの(4)の 回答(5)があり、手動の文字列エスケープを示唆する2番目に賛成の回答が含まれています-安全ではないことが証明されている古いアプローチです。
または、文字列フォーマットの別の方法を提案し、それを究極の万能薬として自慢する、もう少し良い答えがあります。もちろん、そうではありません。この方法は、通常の文字列フォーマットよりも優れているわけではありませんが、すべての欠点があります。文字列にのみ適用可能であり、他の手動フォーマットと同様に、基本的にオプションであり、義務ではなく、あらゆる種類の人為的エラーが発生しやすくなります。
これはすべて、OWASPやPHPマニュアルなどの当局によってサポートされている、「エスケープ」とSQLインジェクションからの保護の平等を宣言する1つの非常に古い迷信によるものだと思います。
PHPのマニュアルが何年にもわたって述べていることに関係なく、*_escape_string
データを安全にすることは決してなく、意図されたこともありません。文字列以外のSQL部分には役に立たないことに加えて、手動エスケープは、自動とは対照的に手動であるため、間違っています。
そしてOWASPはそれをさらに悪化させ、まったくナンセンスなユーザー入力のエスケープを強調します。注射保護の文脈ではそのような言葉があってはなりません。ソースに関係なく、すべての変数は潜在的に危険です!つまり、ソースに関係なく、すべての変数を適切にフォーマットしてクエリに含める必要があります。重要なのは目的地です。開発者が羊を山羊から分離し始めた瞬間(特定の変数が「安全」であるかどうかを考えて)、開発者は災害への第一歩を踏み出します。言うまでもなく、この文言でさえ、エントリポイントでの一括エスケープを示唆しており、非常に魔法の引用機能に似ています。すでに軽蔑され、非推奨になり、削除されています。
したがって、「エスケープ」とは異なり、プリペアドステートメントはSQLインジェクションから実際に保護する手段です(該当する場合)。
パラメータ化されたSQLクエリを実行するには、PDO(PHPデータオブジェクト)を使用することをお勧めします。
これはSQLインジェクションから保護するだけでなく、クエリを高速化します。
そして、PDOではなく、使用することによってmysql_
、mysqli_
およびpgsql_
機能を、あなたは、データベースプロバイダを切り替える必要があること稀で、アプリケーションがもう少しデータベースから抽出します。
PDO
クエリを使用して準備します。
($conn
あるPDO
物体)
$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
ご覧のとおり、プリペアドステートメントを使用することをお勧めします。間違いではありませんが、クエリがプロセスごとに1回だけ実行されると、パフォーマンスがわずかに低下します。
私はこの問題に直面していましたが、ハッカーが引用符の使用を避けるために使用する方法で、非常に洗練された方法で解決したと思います。これをエミュレートされたプリペアドステートメントと組み合わせて使用しました。私はこれを使用して、あらゆる種類のSQLインジェクション攻撃を防ぎます。
私のアプローチ:
入力が整数であると予想される場合は、それが本当に整数であることを確認してください。PHPのような変数型言語では、これは非常に重要です。たとえば、この非常にシンプルで強力なソリューションを使用できます。
sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);
整数の16進数から何か他のものを期待する場合は、それを。ヘックスすると、すべての入力を完全にエスケープします。C / C ++には
mysql_hex_string()
、と呼ばれる関数があり、PHPではを使用できますbin2hex()
。を使用しても
mysql_real_escape_string
、PHPは同じ容量を割り当てる必要があるため、エスケープされた文字列のサイズが元の長さの2倍になることを心配する必要はありません((2*input_length)+1)
。この16進法は、バイナリデータを転送するときによく使用されますが、SQLインジェクション攻撃を防ぐためにすべてのデータで使用しない理由はわかりません。代わりに
0x
、データの前にMySQL関数を追加するか、MySQL関数を使用する必要があることに注意してくださいUNHEX
。
したがって、たとえば、次のクエリを実行します。
SELECT password FROM users WHERE name = 'root';
となります:
SELECT password FROM users WHERE name = 0x726f6f74;
または
SELECT password FROM users WHERE name = UNHEX('726f6f74');
16進数は完璧な脱出です。注入する方法はありません。
UNHEX関数と0xプレフィックスの違い
コメントで議論があったので、いよいよはっきりさせておきたいと思います。これらの2つのアプローチは非常に似ていますが、いくつかの点で少し異なります。
0x
接頭辞は、次のようなデータ列に対してのみ使用することができchar
、varchar
、text
、block
、binary
、など
。また、その使用は、あなたが空の文字列を挿入しようとしている場合は少し複雑です。完全にに置き換える''
必要があります。そうしないと、エラーが発生します。
UNHEX()
任意の列で機能します。空の文字列について心配する必要はありません。
16進法は攻撃としてよく使用されます
この16進法は、整数が文字列のようであり、mysql_real_escape_string
。だけでエスケープされるSQLインジェクション攻撃としてよく使用されることに注意してください。そうすれば、引用符の使用を避けることができます。
たとえば、次のようなことをするだけの場合:
"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])
攻撃は非常に簡単にあなたを注入することができます。スクリプトから返された次の挿入されたコードについて考えてみます。
SELECT ... WHERE id = -1 UNION ALL SELECT table_name FROM information_schema.tables;
そして今、テーブル構造を抽出するだけです:
SELECT ... WHERE id = -1 UNION ALL SELECT column_name FROM information_schema.column WHERE table_name = __0x61727469636c65__;
そして、必要なデータを選択するだけです。かっこいいじゃないですか。
ただし、注入可能なサイトのコーダーが16進数を使用する場合、クエリは次のようになるため、注入はできません。
SELECT ... WHERE id = UNHEX('2d312075...3635');
非推奨の警告: この回答のサンプルコード(質問のサンプルコードと同様)は
MySQL
、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPの拡張機能を使用しています。セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用してください。以下に概説する戦略は、自己責任で使用してください。(また、
mysql_real_escape_string()
PHP 7で削除されました。)重要
SQLインジェクションを防ぐ最善の方法は、受け入れられた回答が示すように、エスケープする代わりにプリペアドステートメント を使用することです。
開発者がプリペアドステートメントをより簡単に使用できるようにするAura.SqlやEasyDBなどのライブラリがあります。プリペアドステートメントがSQLインジェクションの停止に優れている理由の詳細については、この
mysql_real_escape_string()
バイパスと最近修正されたWordPressのUnicodeSQLインジェクションの脆弱性を参照してください。
インジェクション防止-mysql_real_escape_string()
PHPには、これらの攻撃を防ぐために特別に作られた関数があります。あなたがする必要があるのは、一口の関数を使用することだけですmysql_real_escape_string
。
mysql_real_escape_string
MySQLクエリで使用される文字列を受け取り、すべてのSQLインジェクションの試行を安全にエスケープして同じ文字列を返します。基本的に、ユーザーが入力する可能性のある厄介な引用符( ')を、MySQLで安全な代替物であるエスケープされた引用符\'に置き換えます。
注:この機能を使用するには、データベースに接続している必要があります。
// MySQLに接続します
$name_bad = "' OR 1'";
$name_bad = mysql_real_escape_string($name_bad);
$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";
$name_evil = "'; DELETE FROM customers WHERE 1 or username = '";
$name_evil = mysql_real_escape_string($name_evil);
$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;
詳細については、MySQL-SQLインジェクション防止を参照してください。
あなたはこのような基本的なことをすることができます:
$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
これですべての問題が解決するわけではありませんが、非常に優れた足がかりになります。変数の存在、形式(数字、文字など)の確認など、明らかな項目は省略しました。
最終的に使用するものが何であれ、入力がまだmagic_quotes
他の善意のゴミによって破壊されていないことを確認し、必要に応じて、それを実行するstripslashes
か、それをサニタイズするために何かを実行します。
非推奨の警告: この回答のサンプルコード(質問のサンプルコードと同様)は
MySQL
、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPの拡張機能を使用しています。セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用してください。以下に概説する戦略は、自己責任で使用してください。(また、
mysql_real_escape_string()
PHP 7で削除されました。)
パラメータ化されたクエリと入力の検証がその方法です。mysql_real_escape_string()
使用されていても、SQLインジェクションが発生する可能性のあるシナリオは多数あります。
これらの例はSQLインジェクションに対して脆弱です。
$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");
または
$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");
どちらの場合も、'
カプセル化を保護するために使用することはできません。
出典:予期しないSQLインジェクション(エスケープが十分でない場合)
私の意見では、PHPアプリケーション(またはWebアプリケーション)へのSQLインジェクションを一般的に防ぐ最善の方法は、アプリケーションのアーキテクチャについて考えることです。SQLインジェクションから保護する唯一の方法が、データベースと通信するたびに正しいことを行う特別なメソッドまたは関数を使用することを忘れないことである場合、それは間違っています。そうすれば、コードのある時点でクエリを正しくフォーマットするのを忘れるのは時間の問題です。
MVCパターンとCakePHPやCodeIgniterなどのフレームワークを採用するのがおそらく正しい方法です。安全なデータベースクエリの作成などの一般的なタスクが解決され、そのようなフレームワークに一元的に実装されています。これらは、Webアプリケーションを適切な方法で整理し、単一のSQLクエリを安全に構築することよりもオブジェクトのロードと保存について考えるようにするのに役立ちます。
SQLインジェクションやその他のSQLハッキングを防ぐ方法はたくさんあります。インターネット(グーグル検索)で簡単に見つけることができます。もちろん、PDOは優れたソリューションの1つです。しかし、SQLインジェクションからのいくつかの良いリンク防止を提案したいと思います。
PHPでのSQLインジェクションと防止に関するMicrosoftの説明
そして、MySQLとPHPによるSQLインジェクションの防止のような他のいくつか。
では、なぜクエリがSQLインジェクションを行わないようにする必要があるのでしょうか。
私はあなたに知らせたいです:なぜ私たちは以下の短い例でSQLインジェクションを防ごうとするのですか?
ログイン認証の一致のクエリ:
$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";
さて、誰か(ハッカー)が置くなら
$_POST['email']= [email protected]' OR '1=1
とパスワードは何でも...
クエリは、次の場合にのみシステムに解析されます。
$query="select * from users where email='[email protected]' OR '1=1';
他の部分は破棄されます。それで、何が起こりますか?許可されていないユーザー(ハッカー)は、パスワードがなくても管理者としてログインできます。これで、管理者/メール担当者ができることは何でもできます。ほら、SQLインジェクションが妨げられないと非常に危険です。
私はセキュリティの観点からストアドプロシージャを好みます(MySQLは5.0以降ストアドプロシージャをサポートしています)-利点は-
- ほとんどのデータベース(MySQLを含む)では、ユーザーアクセスをストアドプロシージャの実行に制限できます。きめ細かいセキュリティアクセス制御は、特権攻撃の昇格を防ぐのに役立ちます。これにより、侵害されたアプリケーションがデータベースに対して直接SQLを実行できなくなります。
- これらはアプリケーションから生のSQLクエリを抽象化するため、アプリケーションが利用できるデータベース構造の情報は少なくなります。これにより、データベースの基盤となる構造を理解し、適切な攻撃を設計することが困難になります。
- それらはパラメータのみを受け入れるので、パラメータ化されたクエリの利点があります。もちろん、IMOでは、入力をサニタイズする必要があります。特に、ストアドプロシージャ内で動的SQLを使用している場合はそうです。
欠点は-
- それら(ストアドプロシージャ)は維持するのが難しく、非常に迅速に増殖する傾向があります。これにより、それらの管理が問題になります。
- これらは動的クエリにはあまり適していません。動的コードをパラメータとして受け入れるように構築されている場合、多くの利点が無効になります。
誰かがPHPとMySQLまたは他のデータベースサーバーを使いたいと思うなら:
- PDO(PHPデータオブジェクト)の学習について考えてみてください。これは、複数のデータベースにアクセスするための統一された方法を提供するデータベースアクセス層です。
- MySQLiの学習について考えてください
- 次のようなネイティブPHP関数を使用します:strip_tags、mysql_real_escape_string、または変数が数値の場合は、
(int)$foo
。PHPの変数のタイプについて詳しくは、こちらをご覧ください。PDOやMySQLiなどのライブラリを使用している場合は、常にPDO :: quote()とmysqli_real_escape_string()を使用してください。
ライブラリの例:
---- PDO
-----プレースホルダーなし-SQLインジェクションに適しています!悪いです
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");
-----名前のないプレースホルダー
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);
-----名前付きプレースホルダー
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");
--- MySQLi
$request = $mysqliConnection->prepare('
SELECT * FROM trainers
WHERE name = ?
AND email = ?
AND last_login > ?');
$query->bind_param('first_param', 'second_param', $mail, time() - 3600);
$query->execute();
PS:
PDOはこの戦いに簡単に勝ちます。12の異なるデータベースドライバーと名前付きパラメーターのサポートにより、パフォーマンスのわずかな低下を無視して、そのAPIに慣れることができます。セキュリティの観点からは、開発者が想定どおりに使用している限り、どちらも安全です。
しかし、PDOとMySQLiはどちらも非常に高速ですが、MySQLiのベンチマークでは、パフォーマンスがわずかに速くなります。準備されていないステートメントでは最大2.5%、準備されたステートメントでは最大6.5%です。
また、データベースへのすべてのクエリをテストしてください。これは、インジェクションを防ぐためのより良い方法です。
可能であれば、パラメーターのタイプをキャストします。ただし、int、bool、floatなどの単純な型でのみ機能します。
$unsafe_variable = $_POST['user_id'];
$safe_variable = (int)$unsafe_variable ;
mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
RedisやMemcachedなどのキャッシュエンジンを利用したい場合は、DALMPを選択できます。純粋なMySQLiを使用します。これを確認してください:PHPを使用したMySQL用のDALMPデータベース抽象化レイヤー。
また、クエリを準備する前に引数を「準備」して、動的クエリを作成し、最後に完全に準備されたステートメントクエリを作成することができます。PHPを使用したMySQL用のDALMPデータベース抽象化レイヤー。
(mysql_
関数からの)PDOの使用方法がわからない場合のために、単一ファイルである非常に単純なPDOラッパーを作成しました。これは、アプリケーションが実行する必要のあるすべての一般的なことを実行することがいかに簡単であるかを示すために存在します。PostgreSQL、MySQL、およびSQLiteで動作します。
基本的には、それを読んで、あなたがマニュアルを読みながら、店にそれを簡単にかつ形式で値を取得するために、実際の生活の中での使用にPDO機能を配置する方法を確認するためにあなたがしたいです。
単一の列が欲しい
$count = DB::column('SELECT COUNT(*) FROM `user`);
配列(キー=>値)の結果が必要です(つまり、選択ボックスを作成するため)
$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);
単一行の結果が欲しい
$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));
結果の配列が欲しい
$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));
このPHP関数mysql_escape_string()
を使用すると、迅速に適切な予防策を講じることができます。
例えば:
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'
mysql_escape_string
—mysql_queryで使用する文字列をエスケープします
さらに予防するために、最後に追加することができます...
wHERE 1=1 or LIMIT 1
最後に、次のようになります。
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
SQLステートメントで特殊文字をエスケープするためのいくつかのガイドライン。
MySQLを使用しないでください。この拡張機能は非推奨です。代わりにMySQLiまたはPDOを使用してください。
MySQLi
文字列内の特殊文字を手動でエスケープするには、mysqli_real_escape_string関数を使用できます。mysqli_set_charsetで正しい文字セットが設定されていないと、この関数は正しく機能しません。
例:
$mysqli = new mysqli('host', 'user', 'password', 'database');
$mysqli->set_charset('charset');
$string = $mysqli->real_escape_string($string);
$mysqli->query("INSERT INTO table (column) VALUES ('$string')");
プリペアドステートメントで値を自動的にエスケープするには、mysqli_prepareおよびmysqli_stmt_bind_paramを使用します。ここで、適切な変換のために、対応するバインド変数のタイプを指定する必要があります。
例:
$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)");
$stmt->bind_param("is", $integer, $string);
$stmt->execute();
プリペアドステートメントまたはを使用するかどうかに関係なく、使用mysqli_real_escape_string
している入力データのタイプを常に知っている必要があります。
したがって、プリペアドステートメントを使用する場合は、mysqli_stmt_bind_param
関数の変数のタイプを指定する必要があります。
また、の使用はmysqli_real_escape_string
、名前が示すように、文字列内の特殊文字をエスケープするためのものであるため、整数を安全にすることはできません。この関数の目的は、SQLステートメントの文字列の破損、およびそれが引き起こす可能性のあるデータベースへの損傷を防ぐことです。mysqli_real_escape_string
適切に使用すると、特にsprintf
。と組み合わせると便利な関数です。
例:
$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';
$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);
echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5
$integer = '99999999999999999999';
$query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer);
echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647
この問題の簡単な代替策は、データベース自体に適切なアクセス許可を付与することで解決できます。例:MySQLデータベースを使用している場合は、ターミナルまたは提供されているUIからデータベースに入力し、次のコマンドに従ってください。
GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';
これにより、ユーザーは指定されたクエリのみに制限されるように制限されます。削除権限を削除して、PHPページから実行されたクエリからデータが削除されないようにします。2番目に行うことは、MySQLがアクセス許可と更新を更新するように、特権をフラッシュすることです。
FLUSH PRIVILEGES;
フラッシュに関する詳細情報。
ユーザーの現在の権限を確認するには、次のクエリを実行します。
select * from mysql.user where User='username';
GRANTの詳細をご覧ください。
多くの有用な回答に関して、私はこのスレッドにいくつかの価値を追加したいと思っています。
SQLインジェクションは、ユーザー入力(ユーザーが入力し、クエリ内で使用される入力)を介して実行できる攻撃です。SQLインジェクションパターンは正しいクエリ構文ですが、それを呼び出すことができます。悪い理由による悪いクエリ。セキュリティの3つの原則(機密性)に影響を与える秘密情報を取得しようとする(アクセス制御をバイパスする)悪い人がいる可能性があると想定しています。 、整合性、および可用性)。
さて、私たちのポイントは、SQLインジェクション攻撃などのセキュリティの脅威を防ぎ、質問(PHPを使用してSQLインジェクション攻撃を防ぐ方法)をより現実的にすることです。データフィルタリングまたは入力データのクリアは、内部でユーザー入力データを使用する場合です。 PHPやその他のプログラミング言語を使用したこのようなクエリは当てはまりません。または、プリペアドステートメントや現在SQLインジェクション防止をサポートしているその他のツールなどの最新テクノロジーを使用するように多くの人に推奨されているように、これらのツールはもう利用できないと考えてください。アプリケーションをどのように保護しますか?
SQLインジェクションに対する私のアプローチは、データベースに送信する前に(クエリ内で使用する前に)ユーザー入力データをクリアすることです。
(安全でないデータを安全なデータに変換する)ためのデータフィルタリング
PDOとMySQLiは利用できないことを考慮してください。アプリケーションをどのように保護できますか?それらを使用するように強制しますか?PHP以外の他の言語はどうですか?特定の言語だけでなく、より広い境界線に使用できるため、一般的なアイデアを提供することを好みます。
- SQLユーザー(ユーザー特権の制限):最も一般的なSQL操作は(SELECT、UPDATE、INSERT)ですが、それを必要としないユーザーにUPDATE特権を与えるのはなぜですか?たとえば、ログインページと検索ページはSELECTのみを使用しているのに、なぜこれらのページで高い権限を持つDBユーザーを使用するのでしょうか。
ルール:すべての権限に対して1人のデータベースユーザーを作成しないでください。すべてのSQL操作で、(deluser、selectuser、updateuser)のようなスキームをユーザー名として作成して簡単に使用できます。
最小特権の原則を参照してください。
データフィルタリング:クエリユーザー入力を作成する前に、検証してフィルタリングする必要があります。プログラマーにとって、ユーザー入力変数ごとにいくつかのプロパティ(データ型、データパターン、データ長)を定義することが重要 です。(xとy)の間の数値であるフィールドは、正確なルールを使用して正確に検証する必要があります。文字列(テキスト)であるフィールドの場合:パターンの場合、たとえば、ユーザー名には一部の文字のみを含める必要があります。 [a-zA-Z0-9_-。]と言います。長さは(xとn)の間で異なります。ここで、xとn(整数、x <= n)です。 ルール:正確なフィルターと検証ルールを作成することは、私にとってのベストプラクティスです。
他のツールを使用する:ここでは、プリペアドステートメント(パラメーター化されたクエリ)とストアドプロシージャにも同意します。ここでの欠点は、これらの方法では、ほとんどのユーザーには存在しない高度なスキルが必要になることです。ここでの基本的な考え方は、SQLクエリと内部で使用されるデータを区別することです。ここでのユーザー入力データは、(anyまたはx = x)など、元のクエリに何も追加しないため、安全でないデータでも両方のアプローチを使用できます。
詳細については、OWASPSQLインジェクション防止に関するチートシートを参照してください。
上級ユーザーの場合は、この防御を好きなように使用し始めますが、初心者の場合、ストアドプロシージャをすばやく実装してステートメントを準備できない場合は、入力データをできるだけフィルタリングすることをお勧めします。
最後に、ユーザーがユーザー名を入力する代わりに、以下のテキストを送信するとします。
[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'
この入力は、プリペアドステートメントやストアドプロシージャがなくても早期にチェックできますが、念のため、ユーザーデータのフィルタリングと検証の後に使用を開始します。
最後のポイントは、より多くの労力と複雑さを必要とする予期しない動作を検出することです。通常のWebアプリケーションにはお勧めしません。
上記のユーザー入力での予期しない動作は、SELECT、UNION、IF、SUBSTRING、BENCHMARK、SHA、およびrootです。これらの単語が検出されると、入力を回避できます。
更新1:
ユーザーから、この投稿は役に立たないとコメントされました。OWASP.ORGが提供したものは次のとおりです。
主な防御策:
オプション#1:プリペアドステートメントの使用(パラメーター化されたクエリ)
オプション#2:ストアドプロシージャの使用
オプション#3:すべてのユーザー提供入力のエスケープ
追加の防御策:
強制:最小特権
も実行:ホワイトリスト入力検証
ご存知かもしれませんが、記事の主張は、少なくとも1つの参照によって、有効な引数によってサポートされている必要があります。それ以外の場合は、攻撃および悪い主張と見なされます!
アップデート2:
PHPマニュアルから、PHP:プリペアドステートメント-マニュアル:
エスケープとSQLインジェクション
バインドされた変数は、サーバーによって自動的にエスケープされます。サーバーは、エスケープされた値を適切な場所でステートメントテンプレートに挿入してから実行します。適切な変換を作成するには、バインドされた変数のタイプに関するヒントをサーバーに提供する必要があります。詳細については、mysqli_stmt_bind_param()関数を参照してください。
サーバー内の値の自動エスケープは、SQLインジェクションを防ぐためのセキュリティ機能と見なされる場合があります。入力値が正しくエスケープされていれば、プリペアドでないステートメントでも同じ程度のセキュリティを実現できます。
アップデート3:
プリペアドステートメントを使用するときに、PDOとMySQLiがMySQLサーバーにクエリを送信する方法を知るためのテストケースを作成しました。
PDO:
$user = "''1''"; // Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));
クエリログ:
189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\'' 189 Quit
MySQLi:
$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();
クエリログ:
188 Prepare SELECT * FROM awa_user WHERE username =? 188 Execute SELECT * FROM awa_user WHERE username ='\'\'1\'\'' 188 Quit
プリペアドステートメントもデータをエスケープしていることは明らかです。
上記のステートメントでも述べたように、
サーバー内の値の自動エスケープは、SQLインジェクションを防ぐためのセキュリティ機能と見なされる場合があります。入力値が正しくエスケープされている場合、プリペアドされていないステートメントでも同じ程度のセキュリティを実現できます。
したがって、これintval()
は、クエリを送信する前に、などのデータ検証が整数値の良いアイデアであることを証明しています。さらに、クエリを送信する前に悪意のあるユーザーデータを防止することは、正しく有効なアプローチです。
詳細については、この質問を参照してください。PDOは生のクエリをMySQLに送信し、Mysqliは準備されたクエリを送信します。どちらも同じ結果を生成します。
参照:
セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用してください。以下に概説する戦略は、自己責任で使用してください。(また、
mysql_real_escape_string()
PHP 7で削除されました。)非推奨の警告:現時点では、mysql拡張機能は非推奨です。PDO拡張機能の使用をお勧めします
WebアプリケーションがSQLインジェクションに対して脆弱になるのを防ぐために、3つの異なる方法を使用しています。
- 使用
mysql_real_escape_string()
、で事前に定義された関数でPHP、そしてこのコードは、次の文字にバックスラッシュを追加:\x00
、\n
、\r
、\
、'
、"
と\x1a
。SQLインジェクションの可能性を最小限に抑えるために、入力値をパラメーターとして渡します。 - 最も高度な方法は、PDOを使用することです。
これがお役に立てば幸いです。
次のクエリについて考えてみます。
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()はここでは保護しません。クエリ内の変数を一重引用符( '')で囲むと、これから保護されます。これに対する以下の解決策は次のとおりです。
$iId = (int) mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
PDOを使用するのが最良のオプションであることをお勧めします。
編集:
mysql_real_escape_string()
PHP5.5.0で非推奨になりました。mysqliまたはPDOのいずれかを使用してください。
mysql_real_escape_string()の代替は
string mysqli_real_escape_string ( mysqli $link , string $escapestr )
例:
$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");
簡単な方法は、CodeIgniterやLaravelなどのPHPフレームワークを使用することです。これらのフレームワークには、フィルタリングやアクティブレコードなどの機能が組み込まれているため、これらのニュアンスについて心配する必要はありません。
警告:この回答で説明されているアプローチは、非常に特定のシナリオにのみ適用され、SQLインジェクション攻撃はインジェクションが可能であることに依存しているだけではないため、安全ではありませんX=Y
。
攻撃者がPHPの$_GET
変数を介して、またはURLのクエリ文字列を使用してフォームをハッキングしようとしている場合、安全でない場合は攻撃者を捕まえることができます。
RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php
そのため1=1
、2=2
、1=2
、2=1
、1+1=2
、等...攻撃のSQLデータベースへの一般的な質問です。多分それは多くのハッキングアプリケーションによって使用されています。
ただし、サイトから安全なクエリを書き直してはならないことに注意する必要があります。上記のコードは、ハッキング固有の動的クエリ文字列を、攻撃者のIPアドレス、またはそのクッキー、履歴、ブラウザ、その他の機密情報を保存するページに書き換えたりリダイレクトしたりするためのヒントを提供します(ユーザーによって異なります)。情報を提供するため、後でアカウントを禁止するか当局に連絡することで対処できます。
Idiormのようなオブジェクトリレーショナルマッパーを使用することをお勧めします。
$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();
$user->first_name = 'Jamie';
$user->save();
$tweets = ORM::for_table('tweet')
->select('tweet.*')
->join('user', array(
'user.id', '=', 'tweet.user_id'
))
->where_equal('user.username', 'j4mie')
->find_many();
foreach ($tweets as $tweet) {
echo $tweet->text;
}
SQLインジェクションからだけでなく、構文エラーからも節約できます。また、一度に複数の結果と複数の接続にアクションをフィルタリングまたは適用するためのメソッドチェーンを備えたモデルのコレクションもサポートしています。
PHPとMySQLには非常に多くの答えがありますが、SQLインジェクションとoci8ドライバーの定期的な使用を防ぐためのPHPとOracleのコードは次のとおりです。
$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);
非推奨の警告: この回答のサンプルコード(質問のサンプルコードと同様)は
MySQL
、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPの拡張機能を使用しています。セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用してください。以下に概説する戦略は、自己責任で使用してください。(また、
mysql_real_escape_string()
PHP 7で削除されました。)
PDOとMYSQLiを使用することは、SQLインジェクションを防ぐための良い習慣ですが、本当にMySQLの関数とクエリを操作したい場合は、を使用することをお勧めします。
$unsafe_variable = mysql_real_escape_string($_POST['user_input']);
これを防ぐには、さらに多くの機能があります。たとえば、identifyのように、入力が文字列、数値、文字、または配列の場合、これを検出するための組み込み関数が多数あります。また、これらの関数を使用して入力データを確認することをお勧めします。
$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');
$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');
そして、これらの関数を使用して入力データをでチェックする方がはるかに優れていますmysql_real_escape_string
。
私は数年前にこの小さな関数を書きました:
function sqlvprintf($query, $args)
{
global $DB_LINK;
$ctr = 0;
ensureConnection(); // Connect to database if not connected already.
$values = array();
foreach ($args as $value)
{
if (is_string($value))
{
$value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
}
else if (is_null($value))
{
$value = 'NULL';
}
else if (!is_int($value) && !is_float($value))
{
die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
}
$values[] = $value;
$ctr++;
}
$query = preg_replace_callback(
'/{(\\d+)}/',
function($match) use ($values)
{
if (isset($values[$match[1]]))
{
return $values[$match[1]];
}
else
{
return $match[0];
}
},
$query
);
return $query;
}
function runEscapedQuery($preparedQuery /*, ...*/)
{
$params = array_slice(func_get_args(), 1);
$results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.
return $results;
}
これにより、次のようなワンライナーC#風のString.Formatでステートメントを実行できます。
runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);
変数の型を考慮してエスケープします。テーブル、列名をパラメーター化しようとすると、すべての文字列が引用符で囲まれるため失敗します。これは無効な構文です。
セキュリティの更新:以前のstr_replace
バージョンでは、ユーザーデータに{#}トークンを追加することでインジェクションが許可されていました。preg_replace_callback
置換にこれらのトークンが含まれている場合、このバージョンでは問題は発生しません。