ユーザー入力が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;--')
これを防ぐために何ができるでしょうか?
ベストアンサー1
どのデータベースを使用する場合でも、SQL インジェクション攻撃を回避する正しい方法は、データをSQL から分離することです。これにより、データはデータのままになり、 SQL パーサーによってコマンドとして解釈されることはありません。正しくフォーマットされたデータ部分を使用して SQL ステートメントを作成することは可能ですが、詳細を完全に理解していない場合は、常に準備済みステートメントとパラメーター化されたクエリを使用する必要があります。これらは、パラメーターとは別にデータベース サーバーに送信され、解析される 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の場合):
PHP 8.2以降では、execute_query()
1 つのメソッドで SQL ステートメントを準備、パラメータをバインド、実行します。$result = $db->execute_query('SELECT * FROM employees WHERE name = ?', [$name]); while ($row = $result->fetch_assoc()) { // Do something with $row }
PHP8.1まで:
$stmt = $db->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 番目のオプションがあります (たとえば、 PostgreSQLpg_prepare()
のpg_execute()
場合)。PDO は汎用的なオプションです。
接続を正しく設定する
PDO
PDOを使用して MySQL データベースにアクセスする場合、実際の準備済みステートメントはデフォルトでは使用されないことに注意してください。これを修正するには、準備済みステートメントのエミュレーションを無効にする必要があります。PDOを使用して接続を作成する例は次のとおりです。
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
上記の例では、エラー モードは厳密には必要ではありませんが、追加することをお勧めします。このようにして、PDO は をスローすることによってすべての MySQL エラーを通知しますPDOException
。
ただし、必須なのは最初のsetAttribute()
行で、これは PDO にエミュレートされた準備済みステートメントを無効にして、実際の準備済みステートメントを使用するように指示します。これにより、ステートメントと値が MySQL サーバーに送信される前に PHP によって解析されないようにします (攻撃者が悪意のある SQL を挿入する機会を与えません)。
charset
コンストラクタのオプションで設定できますが、PHPの「古い」バージョン(5.3.6以前)では文字セットパラメータを黙って無視しましたDSN で。
MySQLi の
mysqli の場合も同じルーチンに従う必要があります。
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // 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';
}