質問

ユーザー入力が変更を加えずに 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 つのオプションがあります。

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

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('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 番目のオプションがあります (例: pg_prepare() そして pg_execute() PostgreSQLの場合)。PDO はユニバーサル オプションです。

接続を正しく設定する

使用する際の注意点 PDO MySQL データベースにアクセスするには 本物 準備されたステートメントは デフォルトでは使用されません. 。これを修正するには、準備されたステートメントのエミュレーションを無効にする必要があります。PDO を使用して接続を作成する例は次のとおりです。

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

上記の例では、エラー モードは厳密には必要ありません。 しかしそれを追加することをお勧めします. 。こうすることで、スクリプトが次のエラーで停止することはなくなります。 Fatal Error 何か問題が起こったとき。そしてそれは開発者に次の機会を与えます catch 以下のエラー throwとして PDOExceptions.

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

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

説明

何が起こるかというと、渡した 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(array('column' => $unsafeValue));

準備されたステートメントは動的クエリに使用できますか?

クエリ パラメーターにプリペアド ステートメントを使用することはできますが、動的クエリ自体の構造をパラメーター化することはできず、特定のクエリ機能をパラメーター化することもできません。

これらの特定のシナリオでは、可能な値を制限するホワイトリスト フィルターを使用するのが最善の方法です。

// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

他のヒント

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

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

最新バージョンの PHP を使用している場合は、 mysql_real_escape_string 以下に概説するオプションは利用できなくなります(ただし、 mysqli::escape_string は現代の同等物です)。最近では、 mysql_real_escape_string このオプションは、古いバージョンの PHP 上のレガシー コードに対してのみ意味を持ちます。


オプションは 2 つあります。1 つは特殊文字をエスケープする方法です。 unsafe_variable, 、またはパラメータ化されたクエリを使用します。どちらも SQL インジェクションから保護します。パラメータ化されたクエリはより良い方法であると考えられていますが、使用する前に PHP の新しい MySQL 拡張機能に変更する必要があります。

最初に、インパクトの低いストリングをエスケープする方法について説明します。

//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 関数。

パラメータ化されたクエリを使用するには、次を使用する必要があります。 MySQLi ではなく MySQL 機能。例を書き直すには、次のようなものが必要になります。

<?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 このアプローチをとった場合、次の問題に悩まされることになります。 多項式 以下のコメントで。この場合は、整数が引用符で囲まれないため、より複雑になるため、ユーザー入力に数字のみが含まれていることを検証することで対処できます。
  • 私が知らない他のケースもおそらくあります。見つかるかもしれません これ は、遭遇する可能性のあるより微妙な問題のいくつかについて役立つリソースです。

ここでのすべての回答は問題の一部のみをカバーしています。実際には、 動的に追加できるさまざまなクエリ部分:-

  • 文字列
  • 識別子
  • 構文キーワード。

そして、準備されたステートメントはそのうちの 2 つだけをカバーしています。

ただし、場合によっては、クエリをさらに動的にし、演算子や識別子も追加する必要があります。したがって、さまざまな保護技術が必要になります。

一般に、このような保護アプローチは以下に基づいています。 ホワイトリスト登録.

この場合、すべての動的パラメータをスクリプト内にハードコーディングし、そのセットから選択する必要があります。たとえば、動的順序付けを行うには、次のようにします。

$orders  = array("name", "price", "qty"); // Field names
$key     = array_search($_GET['sort'], $orders)); // See if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

ただし、識別子を保護する別の方法、つまりエスケープがあります。識別子を引用符で囲んでいる限り、バッククォートを 2 倍にすることで内部でバッククォートをエスケープできます。

さらなるステップとして、準備されたステートメントから何らかのプレースホルダー (クエリ内の実際の値を表すプロキシ) を使用するという本当に素晴らしいアイデアを借用し、別のタイプのプレースホルダー、つまり識別子プレースホルダーを発明することができます。

長い話を簡単にまとめると、次のようになります。それは プレースホルダー, 、 ない 準備されたステートメント 特効薬として考えることができます。

したがって、一般的な推奨事項は次のように表現できます。プレースホルダーを使用して動的部分をクエリに追加している限り (もちろん、これらのプレースホルダーは適切に処理されています)、クエリが安全であることを確認できます。.

それでも、SQL 構文のキーワード (たとえば、 AND, DESC など)、ただし、この場合はホワイトリスト登録が唯一のアプローチと思われます。

アップデート

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

例えば、 そこ(1) ある(2) まだ(3) たくさん(4) 答え(5), 、 含んでいる 2 番目に賛成票が多かった回答 手動で文字列をエスケープすることをお勧めします。これは安全でないことが証明されている時代遅れのアプローチです。

または、単に提案するもう少し良い答えがあります 文字列フォーマットの別の方法 そしてそれが究極の万能薬であるとさえ自負しています。もちろん、そうではありません。この方法は通常の文字列の書式設定よりも優れているわけではありませんが、その欠点はすべてそのままです。これは文字列にのみ適用でき、他の手動書式設定と同様、基本的にはオプションであり、必須ではないため、あらゆる種類の人的エラーが発生しやすくなります。

私は、これらすべては、次のような権威によって支持された、ある非常に古い迷信のせいだと思います。 オワスプ または PHPマニュアル, これは、「エスケープ」と SQL インジェクションからの保護が同等であることを宣言します。

PHP マニュアルが何年も前から述べてきたことに関係なく、 *_escape_string 決してデータを安全にするものではありません そして決してそうするつもりはありませんでした。文字列以外の SQL 部分には役に立たないだけでなく、手動エスケープは自動エスケープとは対照的に手動であるため、間違っています。

そして OWASP は状況をさらに悪化させ、逃げることを強調します ユーザー入力 それはまったくナンセンスです:注射保護の文脈ではそのような言葉はあってはならない。ソースに関係なく、すべての変数は潜在的に危険です。言い換えると、ソースに関係なく、すべての変数をクエリに入れるために適切にフォーマットする必要があります。重要なのは目的地です。開発者が羊とヤギを分け始めた瞬間(特定の変数が「安全」かどうかを考えながら)、開発者は惨事への第一歩を踏み出します。言うまでもなく、文言さえもエントリポイントで一括エスケープすることを示唆しており、まさに魔法の引用符機能に似ています - すでに軽蔑され、非推奨になり、削除されました。

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

それでも納得できない場合は、私が書いた段階的な説明をここに示します。 SQL インジェクション防止ヒッチハイク ガイド, そこで私はこれらすべての事項を詳細に説明し、悪い慣行とその開示に完全に特化したセクションをまとめました。

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

これにより、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();

ご覧のとおり、多くの人は準備済みステートメントを使用することを推奨しています。それは間違いではありませんが、クエリが実行されると 一度だけ プロセスごとに、パフォーマンスにわずかなペナルティが発生します。

この問題に直面していましたが、次の手順で解決したと思います とても 洗練された方法 - ハッカーが引用符の使用を避けるために使用する方法。これを、エミュレートされた準備済みステートメントと組み合わせて使用​​しました。予防するために使ってます 全て 考えられる SQL インジェクション攻撃の種類。

私のアプローチ:

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

  • 整数に他のものを期待する場合 16進数にしてください. 。16 進数にすると、すべての入力を完全に回避できます。C/C++ には、と呼ばれる関数があります。 mysql_hex_string(), 、PHPでは使用できます bin2hex().

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

  • この 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')

ヘックスは完璧な逃避先です。注射する方法はありません。

UNHEX関数と0xプレフィックスの違い

コメントでも議論があったので、最後にはっきりさせておきたいと思います。これら 2 つのアプローチは非常に似ていますが、いくつかの点で少し異なります。

** 0x** プレフィックスは、次のようなデータ列にのみ使用できます。 char、varchar、text、ブロック、バイナリなど.
また、空の文字列を挿入しようとしている場合、その使用方法は少し複雑になります。それを完全に置き換える必要があります '', 、そうでないとエラーが発生します。

UNHEX() に取り組んでいます どれでも カラム;空の文字列について心配する必要はありません。


Hex メソッドは攻撃としてよく使用されます

この 16 進メソッドは、整数が文字列と同じであり、次の文字列だけでエスケープされる SQL インジェクション攻撃としてよく使用されることに注意してください。 mysql_real_escape_string. 。そうすれば、引用符の使用を避けることができます。

たとえば、次のようなことを行うとします。

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

攻撃はあなたに大きな影響を与える可能性があります 簡単に. 。スクリプトから返された次の挿入コードを考えてみましょう。

選択する ...WHERE ID = -1 Union all select table_name from information_schema.tables

あとはテーブル構造を抽出するだけです。

選択する ...WHERE id = -1 Union all select column_name from information_schema.column where table_name = 0x61727469636c65

あとは必要なデータを選択するだけです。クールじゃないですか?

しかし、注入可能なサイトのコーダーがそれを 16 進数化した場合、クエリは次のようになるため、注入は不可能になります。 SELECT ... WHERE id = UNHEX('2d312075...3635')

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

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

重要

SQL インジェクションを防ぐ最善の方法は、次のようにすることです。 準備されたステートメント 逃げる代わりに, 、 として 受け入れられた答え を示します。

などのライブラリがあります Aura.SQL そして EasyDB これにより、開発者は準備されたステートメントをより簡単に使用できるようになります。プリペアドステートメントが優れている理由をさらに詳しく知るには SQLインジェクションの停止, 、 参照する これ mysql_real_escape_string() バイパス そして 最近、WordPress の Unicode SQL インジェクションの脆弱性が修正されました.

注射の予防 - 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 インジェクションの防止.

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

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

次のような基本的なことを行うことができます。

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

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

最終的に何を使用する場合でも、入力がすでに破壊されていないことを確認してください。 magic_quotes またはその他の善意のゴミを削除し、必要に応じてスルーします。 stripslashes またはそれを消毒するためのもの。

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

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

パラメータ化されたクエリと入力の検証が最適な方法です。SQL インジェクションが発生するシナリオは数多くありますが、 mysql_real_escape_string() 使用されてきました。

これらの例は 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 パターンと次のようなフレームワークを採用する ケーキPHP または コードイグナイター おそらく正しい方法です:安全なデータベース クエリの作成などの一般的なタスクは、このようなフレームワークで解決され、一元的に実装されています。これらは、Web アプリケーションを賢明な方法で編成するのに役立ち、単一の SQL クエリを安全に構築することよりも、オブジェクトの読み込みと保存について考えるようになります。

私は賛成します ストアドプロシージャ (MySQL は 5.0 からストアド プロシージャをサポートしています) セキュリティの観点から見ると、利点は次のとおりです。

  1. ほとんどのデータベース (以下を含む) MySQL) ユーザーのアクセスをストアド プロシージャの実行に制限できるようにします。きめ細かいセキュリティ アクセス制御は、特権昇格攻撃を防ぐのに役立ちます。これにより、侵害されたアプリケーションがデータベースに対して直接 SQL を実行できなくなります。
  2. これらはアプリケーションから生の SQL クエリを抽象化するため、アプリケーションが利用できるデータベース構造の情報が少なくなります。このため、人々がデータベースの基礎構造を理解し、適切な攻撃を設計することが困難になります。
  3. パラメータのみを受け入れるため、パラメータ化されたクエリの利点があります。もちろん、特にストアド プロシージャ内で動的 SQL を使用している場合は、入力をサニタイズする必要があります。

欠点は次のとおりです -

  1. これら (ストアド プロシージャ) は保守が難しく、急速に増殖する傾向があります。このため、それらの管理が問題になります。
  2. これらは動的クエリにはあまり適していません。動的コードをパラメータとして受け入れるように構築されている場合、多くの利点が無効になります。

SQL インジェクションやその他の SQL ハッキングを防ぐ方法はたくさんあります。インターネット(Google検索)で簡単に見つけることができます。もちろん PDO は優れたソリューションの 1 つです。 ただし、SQL インジェクションからリンクを防ぐための優れた方法をいくつか提案したいと思います。

SQL インジェクションとは何か、およびその防止方法

SQLインジェクションのためのPHPマニュアル

PHP における SQL インジェクションと防止に関する Microsoft の説明

そして他のいくつかのような MySQL と PHP による SQL インジェクションの防止

今、 なぜクエリへの SQL インジェクションを防ぐ必要があるのでしょうか?

お知らせいたします:以下の短い例で SQL インジェクションを防止しようとする理由は次のとおりです。

ログイン認証一致のクエリ:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

さて、誰か(ハッカー)が

$_POST['email']= admin@emali.com' OR '1=1

そしてパスワードは何でも....

クエリは次の時点までのみシステムに解析されます。

$query="select * from users where email='admin@emali.com' OR '1=1';

他の部分は破棄されます。それで、何が起こるでしょうか?権限のないユーザー (ハッカー) は、パスワードを持たずに管理者としてログインできます。今では、管理者/電子メール担当者ができることは何でもできるようになりました。SQL インジェクションを防止しないと非常に危険です。

誰かが PHP と MySQL またはその他のデータベース サーバーを使用したい場合は、次のようにすると思います。

  1. 学習について考える PDO (PHP データ オブジェクト) – 複数のデータベースにアクセスするための統一された方法を提供するデータベース アクセス層です。
  2. 学習について考える MySQLi
  3. 次のようなネイティブ PHP 関数を使用します。 ストリップタグ, 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();

追伸:

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 . "')");

キャッシュ エンジンを利用したい場合は、次のようにします。 レディス または Memcached, 、DALMP が選択肢になるかもしれません。純正を使用しております MySQLi. 。これをチェックして: PHP を使用した MySQL 用の DALMP データベース抽象化レイヤー。

また、クエリを準備する前に引数を「準備」して、動的クエリを構築し、最終的に完全に準備されたステートメント クエリを作成することもできます。 PHP を使用した MySQL 用の DALMP データベース抽象化レイヤー。

PDO の使用方法がわからない人のために ( mysql_ 関数)を作りました。 とてもとてもシンプルな PDO ラッパー それは単一のファイルです。これは、アプリケーションで実行する必要がある一般的なことをすべて簡単に実行できることを示すために存在します。PostgreSQL、MySQL、SQLite で動作します。

基本的には読んでください マニュアルを読みながら PDO 関数を実際に使用して、形式での値の保存と取得を簡単にする方法を確認します。 あなた 欲しい。

単一列が欲しい

$count = DB::column('SELECT COUNT(*) FROM `user`);

array(key => value) の結果が必要です (つまり、セレクトボックスを作成するため)

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

詳しくはこちら 付与.

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

非推奨の警告:mysql 拡張機能は現時点では非推奨です。を使用することをお勧めします PDO拡張子

Web アプリケーションが SQL インジェクションに対して脆弱になるのを防ぐために、私は 3 つの異なる方法を使用しています。

  1. の使用 mysql_real_escape_string(), 、これは事前定義された関数です。 PHP, 、このコードは次の文字にバックスラッシュを追加します。 \x00, \n, \r, \, ', " そして \x1a. 。SQL インジェクションの可能性を最小限に抑えるために、入力値をパラメーターとして渡します。
  2. 最も高度な方法は、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() PHP 5.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");

多くの有用な回答に関して、このスレッドにいくつかの価値を追加したいと考えています。SQL インジェクションは、ユーザー入力 (ユーザーによって入力され、クエリ内で使用される入力) を通じて実行できる攻撃です。SQL インジェクション パターンは、次のように呼び出すことができる正しいクエリ構文です。不適切な理由による不適切なクエリでは、セキュリティの 3 原則 (機密性、完全性、可用性) に影響を与える機密情報を (アクセス制御をバイパスして) 取得しようとする悪意のある人物が存在する可能性があると想定されます。

さて、ここでのポイントは、SQL インジェクション攻撃などのセキュリティ脅威を防ぐことです。質問 (PHP を使用して SQL インジェクション攻撃を防ぐ方法) ですが、より現実的に言えば、データ フィルタリングまたは入力データのクリアは、そのような内部でユーザー入力データを使用する場合に当てはまります。 PHP やその他のプログラミング言語を使用したクエリは当てはまりません。あるいは、プリペアド ステートメントや SQL インジェクション防止を現在サポートしているその他のツールなどの最新テクノロジーを使用することが多くの人によって推奨されているように、これらのツールはもう利用できないと考えていますか?アプリケーションをどのように保護しますか?

SQL インジェクションに対する私のアプローチは次のとおりです。ユーザー入力データをデータベースに送信する前に (クエリ内で使用する前に) クリアします。

データフィルタリング(安全でないデータを安全なデータに変換する)それを考慮してください PDO そして MySQLi 利用できない場合、アプリケーションをどのように保護できますか?私にそれらを使用することを強制しますか?PHP 以外の言語についてはどうですか?特定の言語だけでなく、より広い境界線にも使用できるため、一般的なアイデアを提供することを好みます。

  1. SQL ユーザー (ユーザー権限を制限):最も一般的な SQL 操作は (SELECT、UPDATE、INSERT) です。では、UPDATE 権限を必要としないユーザーになぜ UPDATE 権限を与えるのでしょうか?例えば ログインページと検索ページ SELECT のみを使用しているのに、なぜこれらのページで高い権限を持つ DB ユーザーを使用するのでしょうか?ルール:すべての権限に対して 1 つのデータベース ユーザーを作成するのではなく、すべての SQL 操作に対して、使いやすいようにユーザー名として (deluser、selectuser、updateuser) のようなスキームを作成できます。

見る 最小特権の原則

  1. データのフィルタリング:クエリを作成する前に、ユーザー入力を検証してフィルター処理する必要があります。プログラマーにとって、ユーザー入力変数ごとにいくつかのプロパティを定義することが重要です。データ型、データパターン、データ長. 。(x と y) の間の数値であるフィールドは、文字列 (テキスト) であるフィールドに対して、正確なルールを使用して正確に検証される必要があります。たとえば、ユーザー名には一部の文字のみが含まれている必要があります。[a-zA-Z0-9_-.] のようにします。長さは (x と n) の間で変化します。ここで、x と n (整数、 x <=n )。ルール:正確なフィルターと検証ルールを作成することが私にとってのベストプラクティスです。

  2. 他のツールを使用します。ここで、プリペアド ステートメント (パラメータ化されたクエリ) とストアド プロシージャについても同意します。これらの方法の欠点は、ほとんどのユーザーには存在しない高度なスキルが必要であることです。ここでの基本的な考え方は、SQL クエリとデータを区別することです。ここでのユーザー入力データは (any または x=x) などの元のクエリに何も追加しないため、安全でないデータでも両方のアプローチを使用できます。詳細については、こちらをお読みください OWASP SQL インジェクション防止チートシート.

上級ユーザーの場合は、この防御策を自由に使用してください。ただし、初心者の場合、ストアド プロシージャとプリペアド ステートメントをすぐに実装できない場合は、入力データをできるだけフィルタリングすることをお勧めします。

最後に、ユーザーがユーザー名を入力する代わりに、次のテキストを送信すると考えてみましょう。

[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

準備されたステートメントもデータをエスケープしているだけであり、それ以外のものではないことは明らかです。

上の発言にもあるように The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly, したがって、これは、次のようなデータ検証が行われることを証明します。 intval() クエリを送信する前に整数値を指定することは良いアイデアです。また、クエリを送信する前に悪意のあるユーザー データを防ぐこともできます。 正しく有効なアプローチ.

詳細については、この質問を参照してください。 PDO は生のクエリを MySQL に送信し、Mysqli は準備されたクエリを送信します。どちらも同じ結果を生成します。

参考文献:

  1. SQL インジェクションのチートシート
  2. SQLインジェクション
  3. 情報セキュリティー
  4. セキュリティ原則
  5. データ検証

簡単な方法は、次のような 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アドレス, 、またはユーザーの Cookie、履歴、ブラウザー、その他の機密情報も含めて、アカウントを禁止したり当局に連絡したりすることで、後で対処することができます。

答えはたくさんあります PHP と MySQL, 、しかしここにコードがあります PHPとオラクル SQL インジェクションおよび oci8 ドライバーの定期的な使用を防止するために:

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

良いアイデアは、 「オブジェクトリレーショナルマッパー」 のように イディオム:

$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 この拡張子は PHP 5.5.0 で非推奨となり、PHP 7.0.0 で完全に削除されました。

セキュリティ警告:この回答はセキュリティのベスト プラクティスに従っていません。 エスケープは SQL インジェクションを防ぐには不十分です, 、 使用 準備されたステートメント その代わり。以下に概説する戦略はご自身の責任で使用してください。(また、 mysql_real_escape_string() PHP 7 では削除されました。)

使用する PDO そして MYSQLi SQL インジェクションを防ぐには良い方法ですが、MySQL 関数やクエリを実際に使用したい場合は、次の方法を使用することをお勧めします。

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

これを防ぐための機能は他にもあります。識別と同様に、入力が文字列、数値、文字、配列の場合、これを検出するための組み込み関数が多数あります。また、入力データの確認にもこれらの関数を利用するとよいでしょう。

is_string

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

is_numeric

$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 置換にこれらのトークンが含まれている場合、バージョンによって問題は発生しません。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top