PHPでSQLインジェクションを防ぐにはどうすればよいですか?

2008年09月13日に質問されました。  ·  閲覧回数 1.8M回  ·  ソース

Andrew G. Johnson picture
2008年09月13日

次の例のように、ユーザー入力が変更なしで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;--')

これを防ぐために何ができるでしょうか?

回答

Theo picture
2008年09月13日
9124

プリペアドステートメントとパラメータ化されたクエリを使用します。 これらは、パラメータとは別にデータベースサーバーに送信され、データベースサーバーによって解析されるSQLステートメントです。 このように、攻撃者が悪意のあるSQLを挿入することは不可能です。

これを実現するには、基本的に2つのオプションがあります。

  1. PDOの使用(サポートされているデータベースドライバーの場合):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. 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以外のデータベースに接続している場合は、参照できるドライバー固有の2番目のオプションがあります(たとえば、PostgreSQLの場合はpg_prepare()およびpg_execute() )。 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停止することはありません。 また、開発者は、 throw nがPDOException sであるエラーをcatchする機会が与えられます。

ただし、必須なのは最初のsetAttribute()行です。これは、エミュレートされた準備済みステートメントを無効にし、実際の準備済みステートメントを使用するようにPDOに指示します。 これにより、ステートメントと値がMySQLサーバーに送信される前にPHPによって解析されないようになります(攻撃者が悪意のあるSQLを挿入する機会がなくなります)。

コンストラクターのオプションでcharsetを設定できますが、PHPの「古い」バージョン(5.3.6より前)はDSNのcharsetパラメーターを黙って無視することに注意することが重要です。


説明

prepare渡すSQLステートメントは、データベースサーバーによって解析およびコンパイルされます。 パラメータ( ?または上記の例の: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';
}
Matt Sheppard picture
2008年09月13日
1671

非推奨の警告:この回答のサンプルコード(質問のサンプルコードと同様)は、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPのMySQL拡張子を使用しています。

セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。 エスケープは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アプローチを採用すると、以下のコメントで多項式によって説明されている問題が発生します。 整数は引用符で囲まれないため、このケースは扱いにくいため、ユーザー入力に数字のみが含まれていることを検証することで対処できます。
  • 私が気付いていない他のケースがある可能性があります。 これは、発生する可能性のあるより微妙な問題のいくつかに関する有用なリソースであることがわかるかもしれません。
Your Common Sense picture
2011年11月24日
1087

ここでのすべての答えは、問題の一部のみをカバーしています。 実際、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のデフォルトでは、引用符

それでも、SQL構文キーワード( ANDDESCなど)には問題がありますが、この場合はホワイトリストが唯一のアプローチのようです。

したがって、一般的な推奨事項は次のように表現できます。

  • SQLデータリテラルを表す変数(または、簡単に言うと、SQL文字列または数値)は、プリペアドステートメントを介して追加する必要があります。 例外なし。
  • SQLキーワード、テーブル名、フィールド名、演算子など、その他のクエリ部分は、ホワイトリストでフィルタリングする必要があります。

更新

SQLインジェクション保護に関するベストプラクティスについては一般的な合意がありますが、それでも多くの悪いプラクティスがあります。 そして、それらのいくつかは、PHPユーザーの心に深く根付いています。 たとえば、このページには(ほとんどの訪問者には見えませんが) 80を超える削除された回答があります。これらはすべて、品質が悪いか、古くて悪い慣行を促進しているためにコミュニティによって削除されています。 さらに悪いことに、悪い答えのいくつかは削除されず、むしろ繁栄しています。

たとえば、 (1)まだ(3)多くの(4)の回答(5)があり、手動の文字列エスケープを示唆する2番目に賛成の回答が含まれています-安全ではないことが証明されている古いアプローチです。

または、文字列フォーマットの別の方法を提案し、それを究極の万能薬として自慢する、もう少し良い答えが

これはすべて、 OWASPPHPマニュアルなどの当局によってサポートされている、「エスケープ」とSQLインジェクションからの保護の平等を宣言する1つの非常に古い迷信によるものだと思います。

PHPマニュアルが何年にもわたって述べていることに関係なく、 *_escape_stringは決して

そしてOWASPはそれをさらに悪化させ、まったくナンセンスなユーザー入力のエスケープを強調し

したがって、「エスケープ」とは異なり、プリペアドステートメントSQLインジェクションから実際に保護する手段です(該当する場合)。

Kibbee picture
2008年09月13日
857

パラメータ化されたSQLクエリを実行するには、 PDO (PHPデータオブジェクト)を使用することをお勧めします。

これはSQLインジェクションから保護するだけでなく、クエリを高速化します。

また、 mysql_mysqli_ 、およびpgsql_関数ではなくPDOを使用することで、まれに切り替えが必要になる場合に、アプリケーションをデータベースから少し抽象化できます。データベースプロバイダー。

Imran picture
2008年09月13日
626

PDOと準備されたクエリを使用します。

$connPDOオブジェクトです)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
Zaffy picture
2012年10月03日
556

ご覧のとおり、プリペアドステートメントを使用することをお勧めします。 間違いではありませんが、クエリがプロセスごとに1回だけ実行されると、パフォーマンスがわずかに低下します。

私はこの問題に直面していましたが、ハッカーが引用符の使用を避けるために使用する方法で、非常に洗練された方法で解決したと思います。 これをエミュレートされたプリペアドステートメントと組み合わせて使用​​しました。 あらゆる種類のSQLインジェクション攻撃を防ぐために使用しています。

私のアプローチ:

  • 入力が整数であると予想される場合は、それが本当に整数であることを確認してください。 PHPのような変数型言語では、これは非常に重要です。 たとえば、次の非常にシンプルで強力なソリューションを使用できます: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • 整数の16進数から何か他のものを期待する場合は、mysql_hex_string()という関数があり、PHPではbin2hex()使用できます。

    mysql_real_escape_stringを使用しても、PHPは同じ容量((2*input_length)+1)を割り当てる必要があるため、エスケープされた文字列のサイズが元の長さの2倍になることを心配する必要はありません。

  • この16進法は、バイナリデータを転送するときによく使用されますが、SQLインジェクション攻撃を防ぐためにすべてのデータで使用しない理由はわかりません。 データの前に0xを付けるか、代わりに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プレフィックスは、 charvarchartextblockbinaryなどのデータ列にのみ使用できます。
また、空の文字列を挿入しようとしている場合、その使用は少し複雑です。 完全に''に置き換える必要があります。そうしないと、エラーが発生します。

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');
rahularyansharma picture
2011年06月17日
501

非推奨の警告:この回答のサンプルコード(質問のサンプルコードと同様)は、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPのMySQL拡張子を使用しています。

セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。 エスケープはSQLインジェクションを防ぐには不十分です。代わりに、プリペアドステートメントを使用mysql_real_escape_string()はPHP 7で削除されました。)

重要

SQLインジェクションを防ぐ最善の方法は、受け入れられた回答が示すようにエスケープする代わりにプリペアドステートメントを使用することです。

開発者がプリペアドステートメントをより簡単に使用できるようにするAura.SqlEasyDBなどのライブラリがあります。 プリペアドステートメントが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インジェクション防止を参照してください。

Tanerax picture
2008年09月13日
470

あなたはこのような基本的なことをすることができます:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

これですべての問題が解決するわけではありませんが、非常に優れた足がかりになります。 変数の存在、形式(数字、文字など)の確認など、明らかな項目は省略しました。

Rob picture
2008年12月08日
383

最終的に使用するものが何であれ、入力がmagic_quotesまたはその他の意味のあるゴミによってまだ破壊されていないことを確認し、必要に応じてstripslashesなどを実行します。それを消毒します。

Cedric picture
2011年07月04日
365

非推奨の警告:この回答のサンプルコード(質問のサンプルコードと同様)は、PHP 5.5.0で非推奨になり、PHP7.0.0で完全に削除されたPHPのMySQL拡張子を使用しています。

セキュリティ警告:この回答は、セキュリティのベストプラクティスに沿ったものではありません。 エスケープは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インジェクション(エスケープが十分でない場合)