PHPのセキュリティ(XSS、CSRF、SQLインジェクション対策)
カテゴリ:PHP編
入力値検証(データバリデーション)が入り口対策なら、XSS、CSRF対策は出口(出力時)対策となります。 また、SQLインジェクションはデータベースに対する攻撃であり、入り口(SQLクエリ時)対策が必要です。 では順番に解説していきます。XSS(クロスサイトスクリプティング)対策
クロスサイトスクリプティング(サイト間スクリプト実行)は、コメント欄などユーザーからの任意の入力データを表示するページに不正なスクリプトを挿入することで、ページを訪れた人のCookieデータの取得(クッキーハイジャック)やフィッシングサイト等に誘導させる攻撃手法になります。
例えば以下のようなコメントを登録するフォームがあるとします。
コメントの登録フォーム:
<form name="comment_add" action="comment.php" method="post">
<p>コメントの登録:<br><input name="comment" type="textarea"></textarea>
<p><input type="submit" value="送信">
</form>
送信されたデータは通常データベースに保存されますが、例を簡単にするために以下にようにPOSTリクエストを出力する場合で解説します。
コメントの出力コード:
<?php $comment = $_POST['comment']; ?>
<p>コメント:<br><?php echo nl2br($comment); ?>
参考nl2br() 関数は改行文字の前に HTML の改行タグを挿入する関数です。
上記のコードの場合、コメントに不正なJavaScriptが記述された場合、そのスクリプトが実行されてしまいます。 この攻撃の対策は入力データを出力する際にHTMエスケープを実施することです。つまりスクリプトがスクリプトとしてではなく単なる文字列として扱われるようにします。
具体的には不正なスクリプト(原則ユーザーからの入力を出力するものはすべて)が含まれるかもしれない出力変数を htmlentities() 関数を用いてHTMLエンティティに変換させます。
修正後のコメントの出力コード:
<?php $comment = $_POST['comment']; ?>
<p>コメント:<br><?php echo nl2br(htmlentities($comment, ENT_QUOTES, 'UTF-8')); ?>
参考第2引数に指定している ENT_QUOTES はシングルクオートとダブルクオートを共に変換します。第3引数の UTF-8 は文字コードを UTF-8 に変換します。これは日本語のサイトの場合、Shift_JIS などを使用したサイトも存在し、エンコードを統一しなければ正しくエスケープされない場合があるためです。
こうすることで不正なHTMLやスクリプトが挿入されたとしても、それらは単に文字列として表示されるため、Webブラウザ側で実行されることはありません。
CSRF(クロスサイトリクエストフォージェリ)対策
クロスサイトリクエストフォージェリ(サイト間リクエスト偽造)は、Webアプリケーションの脆弱性を利用して、SNSなどにログインしているユーザーに対して罠をしかけ、不正なWebサイト(攻撃サイト)に誘導させ、ユーザーのリクエストを偽造して攻撃する手法です。攻撃者はユーザーの権限を得て、例えばフレンドだけに公開している情報や個人情報などを公開することができるようになります。 この攻撃の大きな特徴は攻撃サイトを作成し、ユーザーをその攻撃サイトに誘導(リンクやボタンをクリック等)させる点です。この罠が成功すると、攻撃対象のサイト(掲示板など)は他サイトからのリクエストをユーザーからのリクエストとして扱い、受け付けてしまします。
従って、この攻撃の対策はサイト外からのリクエストを受信、処理しないように構築することです。 Webページでは、同一ユーザーによるアクセスであるかは判断できず、同一セッションIDであれば同じユーザー(厳密には同じWebブラウザ)として認識してしまいます。
NoteHTTPでは同一クライアント(Webブラウザ)であるかをクッキーに保存しているセッションIDを用いて判別します。これはクッキー情報が盗まれた(セッションハイジャック)場合、成りすましが可能であることを意味します。セションIDを用いて成りすますことを特にセッション・フィクセーション(Session Fixation)と呼びます。
このためセッションID以外の方法でユーザー本人によるリクエストである事を確認する方法が必要です。具体的な対策にはページトークン(ランダムトークン)を用いる方法が挙げられます。もしくは画像認証方式(画像の文字列を入力、適切な画像を選択、画像のパズルを合わせる等)の方法などもあります。
ランダムトークンのためにランダムな文字列を生成する場合は、uniqid() 関数が利用できます。第1引数には rand() 関数、第2引数には一意になる可能性を高めるために、返り値の最後にさらに別のエントロピーを追加するオプションである true を指定します。 最後にランダム文字列を hash_hmac() 関数を用いてハッシュ化します。
ランダムトークンを用いたリクエスト:
//ページトークンの生成
$pagetoken = hash_hmac('sha1', uniqid(rand(), true), 'key');
$_SESSION['pagetoken'] = $pagetoken; //セッションにトークンを保持
<!-- コメントの送信フォーム -->
<form name="comment_add" action="comment.php" method="post">
<p>コメントの登録:<br><input name="comment" type="textarea"></textarea>
<!-- リクエスト時に隠しフィールドでトークンを送信 -->
<input type="hidden" name="pagetoken" value="<?php echo $pagetoken; ?>">
<p><input type="submit" value="送信">
</form>
リクエストを受ける側では、POSTリクエストで受け取ったトークンとセッションに保存したトークンが一致するかを判定し、一致した場合に処理を継続させます。
ランダムトークンを用いた認証:
if (isset($_POST['pagetoken'])) {
if ($_POST['pagetoken'] === $_SESSION['pagetoken']) {
//処理を継続
} else {
//処理を中止してエラーを返す
}
}
先程のコメント登録フォームにページトークンを実装すると以下のようになります。
実装例)
<?php session_start(); ?>
<html>
<?php
//コメントの登録処理
if (isset($_POST['pagetoken'])) {
if ($_POST['pagetoken'] === $_SESSION['pagetoken']) {
$comment = $_POST['comment']; ?>
<p>コメント:<br><?php echo nl2br(htmlentities($comment, ENT_QUOTES, 'UTF-8'));
} else {
echo "不正なリクエストです。" . $_POST['pagetoken'] . " " . $_SESSION['pagetoken'];
}
} else {
//コメントの登録リクエスト
//ページトークンの生成
$pagetoken = hash_hmac('sha1', uniqid(rand(), true), 'key');
$_SESSION['pagetoken'] = $pagetoken; //セッションにトークンを保持
?>
<!-- コメントの送信フォーム -->
<form name="comment_add" action="comment.php" method="post">
<p>コメントの登録:<br><input name="comment" type="textarea"></textarea>
<!-- リクエスト時に隠しフィールドでトークンを送信 -->
<input type="hidden" name="pagetoken" value="<?php echo $pagetoken; ?>">
<p><input type="submit" value="送信">
</form>
<?php } ?>
</html>
参考なお、現在はCakePHPやLaravelなど、Webフレームワーク側でページトークン機能が用意されていますので、Webフレームワークの利用も1つの方法でしょう。
SQLインジェクション対策
SQLインジェクションはSQLインジェクション対策でも触れましたが、データベースへの登録時に不正なSQLコマンドを挿入し、データベースを不正に操作する攻撃です。 対策はデータベースへのクエリ時に値をプレースホルダを用いて指定することです。
PDOモジュールを使用している場合 以下のように bindValue() メソッドでSQL文のパラメータ値とPHP変数をバインドさせます。
例)
$statement = $db->prepare("SELECT * from article WHERE category = :category");
//プレースホルダを用い:categoryに$categoryをバンド
$statement->bindValue(':category', $category, PDO::PARAM_STR);
$statement->execute();
以上、PHPのセキュリティ(XSS、CSRF、SQLインジェクション対策)の解説でした。
公開日時:2020年06月14日 21:51:11