こんにちは、吉岡(@yoshiokatsuneo)です。
インターネットの普及以来、Webサービスにはセキュリティの問題が常につきまとっています。
最近では、個人情報漏洩や、仮想通貨の被害なども増えていますよね。
では、具体的にどのようなセキュリティ問題があるか知っていますか?
のちほど説明しますが、インジェクション問題とクロスサイトスクリプティング(XSS)は、Webエンジニアがまず知っておくべきセキュリティ問題です。
今回は、実際にSQLインジェクションやクロスサイトスクリプティングを実践しつつ、Webエンジニアが最低限知っておくべきセキュリティ対策について実践的に学べる方法を解説します。
近年のセキュリティ動向について
Webのセキュリティ動向について調べたOWASP Top 10というレポートを知っていますか?
OWASP Top 10では、10万以上のWebサービス・APIが調査され、その中から上位10個のセキュリティリスクがまとめられています。以下、最新の2017年版と、その前の2013年版です。
不動の一位がSQLインジェクションをはじめとするインジェクション問題ですね。クロスサイトスクリプティング(XSS)もTop10入りしています。
また、この中でインジェクション問題と、クロスサイトスクリプティングはエンジニアがまず知っておくべきセキュリティ問題です。 インジェクション問題と、クロスサイトスクリプティングは、 単純にプログラムのバグといえるものなので、コードを書くエンジニアが第一に責任をもって対応する必要があります。 他のセキュリティ問題ももちろん重要なのですが、プログラムだけでなく、インフラ、アーキテクチャ、運用、外部連携など 総合的に対策が必要になってくる部分も多く、より広い視野で取り組む必要があります。
こうしたセキュリティ問題は、プログラミングと同じように、実際に作って試しながら学ぶと理解が深まります。ただ、もちろん実際のWebサービスでそんなことを試してしまうと犯罪になりかねないので、簡単には試せませんよね。
自分で試せる環境を用意するにしても、開発環境やデータベースなどの構築が必要です。これが意外と厄介で、手順通りにインストールしたつもりでも、OSやバージョン、他のソフトウェアなど、さまざまな原因でエラーが出たりして失敗することもあります。
そこで、今回はブラウザだけでWeb開発ができるPaizaCloud Cloud IDEを使います。
PaizaCloudでは、さまざまなフレームワークや言語を使ったWeb開発が、初心者でもすぐに始められます。クラウド上の独立したサービスなので、手元のPC環境に影響なく、セキュリティ問題などを気軽に試すことも可能です。
今回は、PaizaCloud上にPHPとMySQLデータベースを使ってSQLインジェクションやクロスサイトスクリプティング(XSS)の脆弱性のあるTodoリストサービスを作り、攻撃を試してみたあと、問題のある部分を直してみましょう。
PHPの動作についても説明しますので、言語問わずプログラミングに少し触れた経験があれば、初心者でも理解できるかと思います。
手順に沿って進めれば、初心者でも10分程度でSQLインジェクションやクロスサイトスクリプティング(XSS)を試せるかと思いますので、ぜひ挑戦してみてください。
PaizaCloud Cloud IDEを使う
それでは、始めていきましょう。
PaizaCloud Cloud IDEのサイトはこちらです。
メールアドレスなどを入力して登録すると、登録確認メールが送られてきます。GitHubやGoogle(Gmail)ログインを利用すると、ボタン一つで登録することもできます。
サーバを作る
まずは、開発環境となるサーバを作りましょう。
「新規サーバ作成」ボタンを押して、サーバ作成画面を開きます。
PHP, phpMyAdmin, MySQLをクリックして「新規サーバ作成」ボタンを押します。
3秒程度で、PHPとMySQLデータベースを使える開発環境がブラウザ上にできあがります。
サンプルのPHPファイルとブラウザが開いています。ここでは必要ないので、これらのウインドウは閉じておきましょう。
データベースを作成
次に、データベースを使ってみましょう。
データベースサーバ(MySQL)はサーバ作成時に設定したので起動していますが、設定していない場合は、以下のように起動しておきます。
$ sudo systemctl enable mysql $ sudo systemctl start mysql
PaizaCloudでは、このようにroot権限でパッケージをインストールすることもできます。
まずはMySQL上に、このアプリケーションで使うデータベースを作成します。
ここでは、mysqlコマンドを使って、"mydb"というデータベースを作ります。
PaizaCloudでは、ブラウザ上でコマンドを入力するための「ターミナル」を使うことができます。
画面左側の「ターミナル」のボタンをクリックします。
以下のコマンドを実行しましょう。
$ mysql -u root create database mydb; exit
データベースが作成できました。
テーブルの作成
続いて、データベース上にテーブルを作成してみましょう。
ターミナル上で、以下のようなコマンドを実行して"todos"という名前のテーブルを作成します。
$ mysql -u root mydb; use mydb create table todos(id int auto_increment primary key not null, name text); exit
テーブルが作成できました。
phpMyAdminの利用
作成したデータベーステーブルのデータは、phpMyAdminでも確認できます。
PaizaCloudで、青いアイコンのPaizaCloudメニューをクリックし、"phpMyAdmin"の"open phpMyAdmin"を選びます。(または、PaizaCloudのブラウザ上で、"http://localhost/phpmyadmin/"と入力します。)
phpMyAdminが表示されました。ここでデータベースの閲覧、編集などができます。開発中にデータベースの内容を確認しながら進めると理解も深まるので、ぜひやってみてください。
セキュリティ問題のあるTodoリストアプリケーション作成
それでは、アプリケーションを作成してみましょう。
今回作成するTodoリストのアプリケーションには、あえてSQLインジェクション、クロスサイトスクリプティングといった問題が含まれています。
どの辺に問題があるか、考えながら作成していきましょう。
PaizaCloudの画面左側のファイル管理ビューを見ると、"public_html/index.php"というファイルが存在しますので、ダブルクリックして開きます。
デフォルトのコードが入っていますが、一度削除して、以下のコードを入れます。
public_html/index.php:
<?php $pdo = new PDO("mysql:host=localhost;dbname=mydb;charset=utf8", "root", "", [PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING] ); if(isset($_POST['submit'])){ // Add an Item $name = $_POST['name']; $sth = $pdo->prepare('INSERT INTO todos (name) values("' . $name . '")' ); $sth->execute(); }elseif(isset($_POST['delete'])){ // Delete an Item $id = $_POST['id']; $sth = $pdo->prepare('DELETE FROM todos WHERE id = ' . $id); $sth->execute(); } ?> <!DOCTYPE HTML> <html> <head> <title>Todo List</title> <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic"> <link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css"> <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css"> </head> <body class="container"> <h1>Todo List</h1> <form method="post"> <input type="text" name="name" value=""> <input type="submit" name="submit" value="Add"> </form> <h2>Current Todos</h2> <?php $keyword = isset($_REQUEST['keyword']) ? $_REQUEST['keyword'] : ''; $sth = $pdo->prepare('SELECT id, name FROM todos WHERE name like "%' . $keyword . '%"'); $sth->execute(); ?> <form> Search: <input type="text" name="keyword" value=""> <input type="submit"> </form> <table class="table table-striped"> <thead><th>Task</th><th></th></thead> <tbody> <?php foreach($sth as $row){ ?> <tr> <td><?= $row[1] ?></td> <td> <form method="POST"> <button type="submit" name="delete">Delete</button> <input type="hidden" name="id" value="<?= $row[0] ?>"> <input type="hidden" name="delete" value="true"> </form> </td> </tr> <?php } ?> </tbody> </table> </body> </html>
ファイルが編集できたら、「保存」ボタンを押すか、「Ctrl-S」または「Command-S」キーで保存します。
コードの概要を見ていきましょう。
まず、PHPの書き方ですが、HTMLの中に"<?php" "?>"や、"<?=" "?>"の囲いを作り、その中にPHPのコードを書きます。書いたコードはHTMLのその場所で実行されます。 "<?= xxx ?>"は"<?php echo xxx ?>"と同じ意味で、実行結果が自動的に表示されます。
「$pdo = new PDO()」では、PHPでデータベースを利用するためのPDOオブジェクトを作成します。引数では、MySQLサーバ名("localhost")、データベース名("mydb")、ユーザ名("root")、パスワード("")を指定しています。
「isset($_POST['submit'])」では、入力フォームで「Todoを追加ボタン」が押された場合かどうかを判定しています。入力フォームから送信された場合、"$_POST['submit']"がtrueになるので、このif文の中身が実行されます。
"$_POST['name'];"には、"name"という名前の入力フォームの値が取得できますので、$nameに設定しておきます。
続いて、SQL文を作成して実行します。SQL文の実行には、prepare()、bindValue()、execute()というPDOの関数を利用します。
prepare()ではデータベースを操作するためのSQL文を設定します。ここでは、"INSERT INTO todos"文でデータベースに行を追加します。
execute()で準備したSQL文を実行します。
「"isset($_POST['delete'])"」のif文では、Todo削除時の動作を記述します。削除ボタンでは"delete"という名前の隠しフォームを用意しているので、"$_POST['delete']"がtrueになり、このif文が実行されます。
"$_POST['id'];"で削除するTodoのidを取得し、SQLの"DELETE FROM todos"文でそのidのtodoを削除します。
<head>タグではmilligramという簡単に表示を綺麗にできるCSSフレームワークを利用するため、<link>タグでスタイルシートを読み込みます。
<body>タグでは、入力フォームを作成します。"name"という名前のテキスト入力フォームと、"submit"という名前のフォーム送信ボタンを作成しています。
テーブル一覧を取得するPHPコードを"<?php"の中に記述します。prepare("SELECT id,name FROM todos WHERE name like \"%" . $keyword . "%\"")でtodosテーブルの一覧を取得するSQLを準備し、execute()でそのSQLを実行します。
LIKEを使ってキーワードで検索できるようにしておきます。
Todo一覧は、<table>タグで作成しています。"table"、"table-striped"というクラスを設定することで、テーブルを綺麗に表示します。
"foreach($sth as $row)"ではtodoを一個ずつ$rowに設定していきます。
テーブルの各行ではtodoの値を表示します。"<?="タグを使い、Todoのname絡むの値となる$row[1]を表示します。
また、削除ボタンを追加するため、各行にフォームを設定します。フォームでは、隠し入力欄でTodoのidとなる"$row['id']"と値として設定しておきます。
最後に、「<?php } ?>」でforeach文を閉じておきましょう。
ページの表示(アプリケーションの実行)
それでは、作成したTodoリストのアプリケーションをブラウザで表示させてみます。
ファイル管理ビューの"public_html/index.php"を右クリックして表示されるメニューから、「ブラウザを実行」を選びます。(または、左側のブラウザアイコンでブラウザを起動してから、URLバーに"http://localhost/~ubuntu/"と入れてエンターキーを押します。
最初はTodoはないですが、"Todo List"ページが表示されています。
Todoの追加や削除もやってみましょう。
動きましたね!これでPHPで作ったTodoリストの完成です!
SQLインジェクションを試してみる
Todoリストのアプリケーションが完成しましたが、このアプリケーションにはセキュリティ的に問題があります。
まずは試してみましょう。検索用のキーワード欄に以下のように入力して検索してみます。
" union select 1,user from mysql.user; —
検索結果の一覧を見ると、最後のMySQLのユーザ一覧が表示されています。このままではSQLの管理者情報が漏洩してしまう問題がありますね。
SQLインジェクションの原因
なぜこのような問題が発生するのでしょうか?
コードの中で、一覧表示に関する部分は以下のようになっています。
$sth = $pdo->prepare("SELECT id,name FROM todos WHERE name like \"%" . $keyword . "%\""); $sth->execute();
ここでキーワードは文字列で連結しているので、例えばキーワードが「ランニング」の場合、以下のようなSQLになります。
SELECT id,name FROM todos WHERE name like "%ランニング%"
このSQLには問題ありません。
では、keywordが「" union select 1,user from mysql.user; —」な場合のSQLを見て見ましょう。
SELECT id,name FROM todos WHERE name like "%" union select 1,user from mysql.user; —%"
なんと、もう一つのSQL文がUNIONで連結されています……。なぜこんなことになっているのでしょうか?
キーワードの最初に「"」が入ることで、検索キーワードの文字列部分を"%"と引用符を閉じてしまい、そのあとは自由にSQL文が書けるようになっています。そこにunionを使って別のSQL文を連結していますよね。最後は「--」というSQLのコメントを利用することで、それ以降の「%"」が無視されるようになってしまっているのですね。
SQLインジェクションを修正する
では、どうすればこの問題を防げるのでしょうか?
問題が発生したのは、SQL文を作るときに引用符「"」がエスケープされていないからです。エスケープするには、「\"」や「""」のように変換する必要があります。
変換用のプログラムを書くことも不可能ではありません。が、さまざまなケースをもれなく網羅して処理できるようにしておく必要がありますし、下手すると別のバグを引き起こしてしまう可能性もあります。
というわけで、自分でエスケープするコードを書くのは避けて、便利なフレームワークやライブラリの機能を利用しましょう。
今回は、以下のようにSQL上に直接文字列を連結するのではなく、「:keyword」として場所だけ指定しておき、bindValue()関数を利用して":keyword"を文字列に置き換えてみます。この置き換えの時に、エスケープ処理をPDO側で自動的にします。
SELECT文を実行しているコードを、以下のように変更してみましょう。
$sth = $pdo->prepare("SELECT id,name FROM todos WHERE name like :keyword"); $sth->bindValue(':keyword', '%' . $keyword . '%', PDO::PARAM_STR); $sth->execute();
Todoの追加を行うINSERT文や、削除を行うDELETE文にも同じ問題がありますので、以下のように変更しておきましょう。
// Add an Item $name = $_POST['name']; $sth = $pdo->prepare("INSERT INTO todos (name) VALUES (:name)"); $sth->bindValue(':name', $name, PDO::PARAM_STR); $sth->execute();
// Delete an Item $id = $_POST['id']; $sth = $pdo->prepare('DELETE FROM todos WHERE id = :id'); $sth->bindValue(':id', $id, PDO::PARAM_INT); $sth->execute();
これで修正できました!
もう一度同じように検索してみましょう。今度はユーザ情報などが漏れることなく、キーワードにマッチするTodoもないので、何も表示されませんでした。
クロスサイトスクリプティング(XSS)を試してみる
このアプリケーションには、クロスサイトスクリプティング(XSS)の脆弱性もあります。
まずは試してみましょう。Todoアイテムの追加フォームで、以下のような名前のアイテムをテキスト入力欄に入れて追加してみます。
<script>alert(window.navigator.userAgent)</script>
追加ボタンを押して一覧表示すると、以下のように利用しているブラウザの名前やバージョンが表示されます。
ここではブラウザ名が表示されているだけです。が、例えばコードを書き換えてクッキーなどからセッション情報を抜き出して他のサーバなどに送信されてしまうと、他のユーザがなりすましでログインできてしまいます。セキュリティ的に大問題ですよね。
クロスサイトスクリプティング(XSS)の原因
なぜこのような問題が発生するのでしょうか?
原因となるコードは以下の部分です。
<td><?= $row[1] ?></td>
Todoリストの名前の内容をそのままHTMLとして表示しています。
例えば「ランニング」の場合は以下のようになります。
<td>ランニング</td>
ランニングなら問題ありませんが、Todoの名前が「<script>alert(window.navigator.userAgent)</script>」の場合は以下のようになってしまいます。
<td><script>alert(window.navigator.userAgent)</script></td>
「<script>」タグが直接埋め込まれているので、HTML文として解釈されてしまい、「<script>」タグの中のJavaScriptコード「alert(window.navigator.userAgent)」が実行されてしまっているんですね。
クロスサイトスクリプティング(XSS)を修正する
では、このクロスサイトスクリプティングの問題はどうすれば防げるでしょうか?
この問題は、HTMLのタグ「<」がそのままHTML中で表示されていることが原因となっています。そのため、「<」を「&lt;」などにエスケープすれば防げます。
この場合も、変換用のプログラムを書くことも不可能ではありませんが、いろいろ考慮しないといけなくて大変なので、便利なフレームワークやライブラリの機能を利用しましょう。
ここでは、Todoの値を直接表示するのではなく、以下のように「htmlspecialchars()」関数を利用して「<」などをエスケープし、HTMLとして解釈されないようにします。
<td><?= htmlspecialchars($row[1]) ?></td>
これで修正できました。
ブラウザで表示させてみましょう。
今度はJavaScriptコードが実行されることなく、入力した値がそのまま表示されています。これでひとまず想定していた問題はなくなりました。
まとめ
というわけで、面倒な開発環境などをすることなくPaizaCloudだけでPHPとDBを使ったアプリケーションを作って、SQLインジェクションとクロスサイトスクリプティング(XSS)の脆弱性を確認・修正までできました。
最近のWeb開発では、セキュリティ面も考慮されたフレームワークを利用するケースが多いので、このような問題は発生しにくくなっています。
ただ、Web開発をするのであれば、基本的なセキュリティ面におけるリスクはしっかり理解しておかなければなりません。
今回のようにサービス開発から問題発生・解決までを試してみると理解が深まりますし、どこにリスクが隠れているかを察知する感覚も磨かれますので、ぜひ挑戦してみてください。
また、paizaラーニングでは「DB/SQL入門編」を公開しています。
SQLのオンライン実行環境と構築済みのデータベースが用意されているので、面倒な環境構築やデータベースの準備などをしなくても、動画と演習問題を通して楽しくSQLの使い方が学べます。
このように、自分で手を動かして実際のSQL文を打ち込んでみたり、結果を確認したりできます。
paizaラーニングの「DB/SQL入門編」について、詳しくはこちら
「PaizaCloud」は、環境構築に悩まされることなく、ブラウザだけで簡単にウェブサービスやサーバアプリケーションの開発や公開ができます。
「paizaラーニング」では、未経験者でもブラウザさえあれば、今すぐプログラミングの基礎が動画で学べるレッスンを多数公開しております。
そして、paizaでは、Webサービス開発企業などで求められるコーディング力や、テストケースを想定する力などが問われるプログラミングスキルチェック問題も提供しています。
スキルチェックに挑戦した人は、その結果によってS・A・B・C・D・Eの6段階のランクを取得できます。必要なスキルランクを取得すれば、書類選考なしで企業の求人に応募することも可能です。「自分のプログラミングスキルを客観的に知りたい」「スキルを使って転職したい」という方は、ぜひチャレンジしてみてください。