XAMPPでMySQLを使う その3
北本です。
前回は以下のようなPHPコードを紹介し、致命的な欠陥があると書いて終わりました。今回はその内容に具体的に触れていくことにします。前回からの続きなのでタイトルにはXAMPPやMySQLと書いていますが、今回の内容はそれらに特化したものではなくSQL・PHP全般に該当する内容かと思います。
test.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <html> <head><title>TEST</title></head> <body> <?php $prefecture = $_GET["prefecture"]; $population_min = $_GET["population_min"]; $population_max = $_GET["population_max"]; $pdo = new PDO("mysql:host=127.0.0.1;dbname=testdb;charset=utf8mb4;", "root", ""); $sql = "SELECT name, prefecture, population FROM cities"; $where = ""; if(!empty($prefecture)){ $where .= " prefecture = '" . $prefecture . "'"; } if(!empty($population_min )){ if(!empty($where)){ $where .= " AND"; } $where .= " population >= " . $population_min; } if(!empty($population_max)){ if(!empty($where)){ $where .= " AND"; } $where .= " population <= " . $population_max; } if(!empty($where)){ $sql .= " WHERE " . $where; } $stmt = $pdo->query($sql); $result = $stmt->fetchAll(); foreach ( $result as $row ) { echo "名前: {$row["name"]}, 都道府県: {$row["prefecture"]}, 人口: {$row["population"]}<br>"; } ?> </body> </html> |
早速ですが、欠陥が明らかになる操作をしてみます。都道府県のテキストボックスに以下の文字列を入力して「送信」ボタンをクリックしてみましょう。
1 | '; UPDATE cities SET prefecture = '大阪府' WHERE name = '尼崎市 |
何もない画面に遷移するはずです。
ではcitiesテーブル見てみましょう。
なんと、尼崎市が大阪府に編入されてしまっています!
というのも、以下のようなSELECT文とUPDATE文が実行されてしまったからです。
1 | SELECT name, prefecture, population FROM cities WHERE prefecture = ''; UPDATE cities SET prefecture = '大阪府' WHERE name = '尼崎市' |
何もない画面が表示されたのは、SELECT文でprefecture = ”に該当するレコードをcitiesから取得しようとして1件も取得できなかったからです。そして、2つ目のUPDATE文が尼崎市のprefectureが大阪府に変更されてしまった原因です。例ではUPDATE文を実行させましたが、やろうと思えばINSERT文やDELETE文を忍び込ませてレコードの追加や削除をしたりテーブル自体を削除してしまったりすることもできるでしょう。
単純に文字列連結でSQL文を生成しているとこのような問題が起きてしまいます。典型的なSQLインジェクションの例です。
これを防ぐには以下のようなコードにします。
test.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | <html> <head><title>TEST</title></head> <body> <?php $prefecture = $_GET["prefecture"]; $population_min = $_GET["population_min"]; $population_max = $_GET["population_max"]; $pdo = new PDO("mysql:host=127.0.0.1;dbname=testdb;charset=utf8mb4;", "root", ""); $sql = "SELECT name, prefecture, population FROM cities"; $where = ""; if(!empty($prefecture)){ $where .= " prefecture = :prefecture"; } if(!empty($population_min )){ if(!empty($where)){ $where .= " AND"; } $where .= " population >= :population_min"; } if(!empty($population_max)){ if(!empty($where)){ $where .= " AND"; } $where .= " population <= :population_max"; } if(!empty($where)){ $sql .= " WHERE " . $where; } $stmt = $pdo->prepare($sql); if(!empty($prefecture)){ $stmt->bindValue(":prefecture", $prefecture, PDO::PARAM_STR); } if(!empty($population_min)){ $stmt->bindValue(":population_min", $population_min, PDO::PARAM_INT); } if(!empty($population_max)){ $stmt->bindValue(":population_max", $population_max, PDO::PARAM_INT); } $stmt->execute(); $result = $stmt->fetchAll(); foreach ( $result as $row ) { echo "名前: {$row["name"]}, 都道府県: {$row["prefecture"]}, 人口: {$row["population"]}<br>"; } ?> </body> </html> |
このコードだと、同様の文字列をテキストボックスに入力して送信しても、尼崎市のprefectureは大阪府に変更されないはずです。
変更点を見ていきましょう。
まず、パラメータの文字列をそのまま連結してwhere句を作成していましたが、そのパラメータの箇所を「:prefecture」、「:population_min」、「:population_max」といったプレースホルダに置き換えています。今回は「:」で開始する名前付きプレースホルダを使用していますが、「?」を使うことも可能です(詳細は次回に)。
また、PDOのqueryメソッドでSQL文を即時実行していたのをprepareメソッドを使用する形に変更しています。prepareメソッドを実行するとPDOStatementオブジェクトが返ってきますが、そのオブジェクトのexecuteメソッドを呼ぶとprepareでセットしたSQL文が実行されます。
executeを実行する前には、bindValueメソッドを実行していますが、ここでプレースホルダに値をセットしています。bindValueメソッドの第1引数については、名前付きプレースホルダを使用している場合はその文字列を指定します。第2引数にはプレースホルダにセットする値、第3引数は任意ですが値の型を指定します。
このようにbindValueでパラメータを反映するようにすれば、
1 | '; UPDATE cities SET prefecture = '大阪府' WHERE name = '尼崎市 |
のような値がテキストボックスに入力されていたとしても、その全体がシングルクオートで囲われた一つの文字列のように処理されるため、先程のようにUPDATE文が実行されてしまうようなことは起こりません。
次回はこれまでの内容の補足的なことを書こうと考えています。今回同様SQL・PHP全般に該当する内容になると思います。