検索サイトを作りたいなと思い、ちょいちょいPHPやSQLについて勉強したり、コードを書いたりしているのですが、PHPでプレースホルダを使用するのに少し手間どったのでその備忘録です。
データベースとSQLインジェクション
データベース(SQL)に関する脅威といえば、みなさまご存じSQLインジェクション。
不正な入力を行うことでデータベースを操作したり、データを不正に取得したりする攻撃です。
主な対策としては、
・SQL文の組み立てをプレースホルダで行う
・エスケープ処理を行う
などがあります。
具体的な内容とか対策とかについては、以下のIPAの資料などを読んでください。
・安全なSQLの呼び出し方(PDF)
検索サイトとアウトなコード
実装の調査用に、こんな感じの簡易データベース表示サイトを作りました。
(データについては英語版WikipediaのNPBの記事より。実行環境はMAMP。)
球団名を部分一致で検索できる入力フォームを付けています。
以下は球団名に「ス」を含む球団を検索した結果。
文字列の部分検索にはLIKEを使用します。SQL文は以下。
1 2 3 |
select 列名 from テーブル名 where 列名 like '%検索文字列%'; // 部分一致 select 列名 from テーブル名 where 列名 like '検索文字列%'; // 前方一致 select 列名 from テーブル名 where 列名 like '%検索文字列'; // 後方一致 |
今回の検索文字列を受け取って、SQLの実行と結果を取得するPHPのコードが以下です。
HTMLで入力された値を確認も何もしていないうえに、そのままSQL文に入れています。ちなみにクライアント(HTML、JavaScriptは使ってない)側でも入力チェックなどは特に何もしてません。SQLインジェクションしてくれと言わんばかりのスリーアウトなコードです。野球だけに。
1 2 3 4 5 6 7 8 |
$word = $_GET['word']; // HTMLから入力受付 $sql = "select * from teams where team like'%".$word."%';"; $re = $s->query($sql); while($row = $re->fetch()){ $rows[] = $row; } |
これにプレースホルダを適用してセーフなコードにします。
PHPでプレースホルダを使用する
『PHP プレースホルダ』とかで検索すると、prepareメソッドを使用することが分かりました。
いろいろ出てきたコードを基に、上記のコードを修正してみました。
1 2 3 4 5 6 7 8 |
$word = $_GET['word']; // HTMLから入力受付 $stmt = $s->prepare("select * from teams where team like %?%;"); $params = (1, "%".$word."%"); $re = $stmt->execute($params); while($row = $re->fetch()){ $rows[] = $row; } |
が、上手くいかないというか画面に何も表示されず真っ白になる。
更にいろいろ調べたところ、LIKEの場合のprepareの書き方が間違っていたらしい。
1 2 3 4 |
// 間違っている書き方 $stmt = $s->prepare("select * from teams where team like %?%;"); // 正しい書き方 値をセットする時に%%を使うっぽい $stmt = $s->prepare("select * from teams where team like ?;"); |
他にもたくさん間違っていたところがありました(bindValueを使用したものとか、元々のコードと混乱したのが原因)。
正しいコードはこちら。
1 2 3 4 5 6 7 8 9 |
$word = $_GET['word']; // HTMLから入力受付 $stmt = $s->prepare("select * from teams where team like ?;"); // <-正しい書き方 $params = ("%".$word."%"); // <-正しいセットの仕方 $stmt->execute([$params]); // <-配列で渡さないとダメ while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $rows[] = $row; } |
ちゃんとプレースホルダ未使用の時と同じような表示結果が得られました!
なおbindValueを使う場合にはexecuteの引数が不要。
1 2 3 4 5 6 7 8 9 |
$word = $_GET['word']; // HTMLから入力受付 $stmt = $s->prepare("select * from teams where team like ?;"); // <-正しい書き方 $stmt->bindValue(1, "%".$word."%"); $stmt->execute(); while($row = $stmt->fetch()){ $rows[] = $row; } |
コード全体
一応PHPとHTMLのコード全体も載せておきます。PDOのテーブル名、ID、PASSWORDには実際の値を入れてください。
調査用の簡易的なものなので、いろいろ雑なのはスルーしてください。
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 |
<?php try{ $s = new PDO("mysql:host=localhost;dbname=テーブル名","ID","PASSWORD"); // 値適宜入れる $s->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); $word = $_GET['word']; // HTMLから入力受付 $stmt = $s->prepare("select * from teams where team like ?;"); // プレースホルダ $params = ("%".$word."%"); // <-値のセット $stmt->execute([$params]); // <-SQL実行 while($row = $stmt->fetch(PDO::FETCH_ASSOC)){ $rows[] = $row; } } catch(PDOException $e){ print "エラー内容:".$e->getMessage(); } ?> <!DOCTYPE html> <html> <head> <title></title> <meta charset="utf-8"> </head> <body> <form action="" method="GET"> <div> 球団名:<input type="text" name="word" value="<?php echo $_GET['word']?>"> </div> </form> <table border="1" style='white-space: nowrap'> <tr style="background-color: lightskyblue"> <th>球団名</th> <th>リーグ</th> <th>本拠地</th> <th>設立年月日</th> </tr> <?php foreach($rows as $row){ ?> <tr> <td><?php echo $row['team']?></td> <td><?php echo $row['league']?></td> <td><?php echo $row['city']?></td> <td><?php echo $row['founded']?></td> </tr> <?php } ?> </table> </body> </html> |
データベースはこんな感じでした。
セリーグ球団の設立年というか設立順は知ってたけど、パリーグの方は全然知らなかったので勉強になりました。あとみんな11月~2月の間なんだなーと思ったけど単にオフシーズンってだけですね。
今年は結構荒れそうなので楽しみ半分不安半分だ……。
参考
以下のサイトを参考に解決することができました。ありがとうございます。
コメント