目次
- 注意
- はじめに
- SQLインジェクションとは
- 対策
- 誤った対策
- おわりに
どうも、普段はPHPばっかり書いてるumiushiです。
今回から、何回かに分けて「PHPで記述しているときに埋め込みやすい脆弱性」についての解説と、その対策法について語っていきたいと思います。
小むづかしい話もするかもしれませんが、最後までお付き合いいただけたら幸いです。
- SQLインジェクション編 ←ココ
- (2019年8月14日 追加)クロスサイトスクリプティング(XSS)編
- (まだ)クロスサイトリクエストフォージェリ(CSRF)編
- 残りは未定
注意
本記事で紹介する内容は、悪用することで容易にセキュリティインシデントを引き起こす内容が含まれています。
本記事の内容をもとに、自分が管理していないサーバなどへ攻撃をした場合、何かしらの法的措置が行われる可能性がありますので、絶対に行わないようにしてください。
はじめに
近年[いつ?]プログラムの脆弱性をついた攻撃が多くなってきている(ように思う)。
プログラムを書くときには、仕様に沿うように書くのは当然ではあるが、セキュリティにも気をつけて記述する必要がある。
本記事では、発生頻度が高いと思われる脆弱性について、その解説とPHPにおける「脆弱性を発生させないプログラミング」すなわちセキュアプログラミングについて解説する。
PHPerなりたての人や、普段セキュリティをあまり意識せずにプログラミングしている人が対象である。
本記事はPHPでのプログラミングを想定した記事としているが、脆弱性の原理さえわかれば、他の言語でも同様に対応できる(はず)。
SQLインジェクションとは
SQLインジェクションとはSQLクエリの中に、(SQLで記述された)意図しないコードを埋め込む攻撃手法のことである。
この攻撃が実行された場合、データベースに登録された様々なデータが閲覧された
り、勝手に追加・更新されたり、最悪はデータベースそのものを削除されたりする。
SQLインジェクションは、ユーザから何かしらのデータを受け取り、その内容をSQLクエリ内で使用する場合に発生することがある。
例えばブログタイトルを検索するSQLクエリを考えてみると、次のようなものになる。
$query = "SELECT * FROM blogs WHERE title = 'エクセレントなタイトル'";
しかし、上記のクエリでは固定のタイトルしか検索できない。
そこでユーザが自由に検索できるよう検索ボックスを実装し、SQLクエリを次のように書き換えてみる。
(ユーザが入力した検索ワードは$search_query
に入るものとする)
$query = "SELECT * FROM blogs WHERE title = '$search_query'";
このコードではユーザが入力した検索ワードを、疑いもせずにSQLクエリに埋め込んでいる。
もしユーザが
' or 'a' = 'a' --'
のような検索ワードを入力した場合、先のSQLは次のように解釈される。
SELECT * FROM blogs WHERE title = '' or 'a' = 'a' --''
SQLでは--
以降はコメントとして無視されてしまうため、title = ''と'a' = 'a'
がor
で検索されることになる。
当然であるが'a' = 'a'
は常に真であるためor
も常に真となってしまい、結果として検索ワードにかかわらずすべてのデータを読み取られる結果となってしまう。
対策
ユーザからのデータをSQLクエリに直接埋め込まず、プリペアドステートメントを使用する
または
mysqli::real_escape_string()
などのエスケープ関数を使用する
ことでSQLインジェクションの脆弱性を含まないコードにできる。
プリペアドステートメント
先程の例で検索クエリを実装する場合、次のように実装する。
$pdo = new \PDO(ここに接続情報); $query = "SELECT * FROM blogs WHERE title = ?"; // プリペアドステートメントを用意する $stmt = $pdo->prepare($query); // プリペアドステートメントに値をバインドして実行する $stmt->execute([$search_query]); // 実行結果をfor文で処理する foreach ($stmt->fetchAll() as $blog) { // 好きな処理をここに }
こうすることにより、ユーザデータに危険な文字列が含まれていたとしても、安全にSQLを実行することができるようになる。
なお今回は、疑問符パラメータを使用しているが、SQL文中の?
は、:title
のような名前付きパラメータにすることもできる。
その場合execute()
には連想配列を渡すことで値をバインドできる。
また、サンプルプログラムではPDOを使用しているが、mysqliなどの他のデータベース操作用モジュールでも同じようにプリペアドステートメントを発行できる。
エスケープ関数
mysqliモジュールにはエスケープ関数としてreal_escape_string()
、escape_string()
が用意されている。
ユーザからのデータを上記関数を使用してエスケープ処理を行うことで、安全に実行できるようになる。
$mysqli = new mysqli(ここに接続情報); // エスケープ処理 $search_query = $mysqli->real_escape_string($search_query); // エスケープ処理を行ったので安全に実行できる $mysqli->query("SELECT * FROM blogs WHERE title = $search_query");
注意点として、この関数を使用する場合は接続先データベースの文字コードを適切に設定する必要がある。
もし文字コードが適切でない場合、上記のエスケープ関数は正しくエスケープしてくれなくなる。
例ではMySQL用で解説したが、PostgreSQL用の関数も用意されているので、必要な場合はPHPの公式ドキュメントを参照してほしい。
誤った対策
PHPにはaddslashes()
という関数が用意されている。
この関数は引数に含まれる、シングルクォート'
、ダブルクォート"
、バックスラッシュ\
、NUL(NULLバイト)の前にバックスラッシュを付与して返す。
一見するとこの関数でエスケープ処理ができそうである。
しかし、この関数の主機能は特定の文字の前にバックスラッシュを付与するというものであり、危険なクエリに対するエスケープ処理を目的としたものではない。
そのため、正しくエスケープされることが保証されず、思わぬところで動作してくれないケースが発生する。
例えば次のケースを考える。
文字コードがShift_JIS(CP932でもいい)のとき次の文字列が入力されたとする。
\x83'
このデータに対してaddslashes()
を実行した場合、末尾のシングルクォートの手前にバックスラッシュが付与されて次のような内容となる(\x5c
はShift_JISにおけるバックスラッシュの文字コード)。
\x83\x5c'
ここで、\x83\x5c
はShift_JISにおいてはカタカタのソ
として扱われるため、上記の内容は次のように解釈される。
ソ'
おわかりいただけただろうか。
addslashes()
を使用したにもかかわらず、シングルクォートがエスケープされていないことを。
このように文字コードに次第でaddslashes()
は適切に動作しなくなるばかりか、存在しない文字を生み出す原因となりうるため、SQLクエリのエスケープ処理には適していない。
なお、この問題はShift_JISにおける末尾が5c
となる文字(いわゆるダメ文字)全てで発生する可能性がある。
SQLのエスケープを行うという目的のときはaddslashes()
を使わず、PDOのプリペアドステートメントやmysqliのreal_escape_string()
などを使うようにするべきである。
おわりに
今回はSQLインジェクションについて解説したが、この脆弱性はWebシステム以外でも発生しうるものである。
データベースを使用しており、プログラム内部でクエリを組み立てている場合、発生する可能性がある。
また発生したときの被害は、データ漏洩や消失、改ざんなど、割とシャレにならないことも多い。
そのため、クエリを組み立てる必要があるときは、常にSQLインジェクションの可能性を考慮する必要があるといえる。