PHP - WEB - 14. htmlspecialchars関数
引き続きセキュリティについて学習していきましょう。ここではスクリプト挿入攻撃という具体的な攻撃手法とその原因について取り上げます。
Webアプリケーションは、悪意のある利用者によって入力フォームなどから不正なスクリプトを入力される可能性があります。ここでいうスクリプトとは現在ではJavaScriptプログラムなどが一般的でしょう。
スクリプト挿入攻撃とは、脆弱性のあるWebアプリケーションにおいて、悪意のある利用者の入力したJavaScriptプログラムが、意図せずに他の利用者のWebブラウザ上で実行されてしまうようなケースです。JavaScriptプログラムはWebブラウザ上のCookieにアクセスしたり、外部のサイトにデータを送信したり、様々な用途に利用できるため、このような脆弱性を作らないようにWebアプリケーションを開発しないといけません。
スクリプト挿入攻撃を応用した攻撃手法にクロスサイトスクリプティングがあります。まずはスクリプト挿入攻撃の原因と対策を学習していきましょう。
PHPプログラムの開発(チャットプログラム)
ここではPHPで簡単なチャットプログラム( chat.php
)を作成して、スクリプト挿入攻撃の原因と対策について確認していきましょう。
<?php
$file = "chat.txt";
$messages = file($file, FILE_IGNORE_NEW_LINES);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$message = (string)filter_input(INPUT_POST, "message");
if ($message !== "") {
$messages[] = $message;
file_put_contents($file, $message . PHP_EOL,
FILE_APPEND | LOCK_EX);
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Chat</h3>
<hr>
<form action="chat.php" method="post">
<input type="text" name="message">
<input type="submit" value="send">
</form>
<ul>
<?php for ($i=0; $i < count($messages); $i++) { ?>
<li><?php echo $messages[$i]; ?></li>
<?php } ?>
</ul>
</body>
</html>
このプログラムを実行する前にテキストエディタで
chat.txt
という空のファイルを作成しておく必要があります。
このプログラムはチャット画面の表示処理と、メッセージの投稿処理の2つの役割を1つのファイルで実装しています。まずはチャット画面を構成するHTMLのコード部分から見ておきましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Chat</h3>
<hr>
<form action="chat.php" method="post">
<input type="text" name="message">
<input type="submit" value="send">
</form>
<ul>
<?php for ($i=0; $i < count($messages); $i++) { ?>
<li><?php echo $messages[$i]; ?></li>
<?php } ?>
</ul>
</body>
</html>
このプログラムは form
タグで入力フォームを定義しています。 form
タグの action
属性には chat.php
、 method
属性には post
と指定してるので、送信ボタンをクリックしたときには再び chat.php
ファイルにPOSTリクエストが送信されるようになっています。
また form
タグの定義の後には ul
タグと li
タグによって配列変数 $messages
の要素を for
文で繰り返しながらでリスト形式で出力しています。
次にプログラムの先頭部分を見てみましょう。
<?php
$file = "chat.txt";
$messages = file($file, FILE_IGNORE_NEW_LINES);
ここでは file
関数を使って chat.txt
というファイルを読み込んでいます。これにより $messages
には chat.txt
の行データが配列データとして代入されます。
次に if
文を使って条件分岐を定義しています。
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SERVER
変数は $_GET
や $_POST
と同様にスーパーグローバル変数と呼ばれる特別な変数の一つです。 $_SERVER
変数にキーを指定することで、サーバ上の様々な情報にアクセスできます。ここでは $_SERVER["REQUEST_METHOD"]
とすることで現在処理中のリクエストがPOSTリクエストかどうかを判定します。
処理中のリクエストがPOSTリクエストの場合は以下のように処理します。
$message = (string)filter_input(INPUT_POST, "message");
if ($message !== "") {
$messages[] = $message;
file_put_contents($file, $message . PHP_EOL,
FILE_APPEND | LOCK_EX);
}
ここではまず filter_input
関数を使ってリクエストパラメータから "message"
という名前のパラメータを取得しています。また戻り値を (string)
として文字列型にキャストしているので、リクエストパラメータの中に "message"
パラメータが存在しない場合は、 filter_input
関数の戻り値は ""
空文字になります。
その後 if
文で $message
が空文字でないことを確認した後、配列変数 $messages
に $message
を追加して、最後に file_put_contents
関数によって chat.txt
ファイルに新規メッセージ( $message
)を追記しています。このとき LOCK_EX
という定数によってファイル書き込み時に競合が発生しないように排他ロックを取得しています。
ここまで作成してきたチャットプログラム( chat.php
)にはスクリプト挿入攻撃の脆弱性があります。実際にビルトインWebサーバを起動して動作を確認してみましょう。
$ php -S localhost:8000
次にここでは動作確認のために2人のユーザを模倣して、Webブラウザを2つ起動しておきましょう。ここではGoogle ChromeとFirefoxの2つのアプリケーションを起動します。また説明上、Google ChromeをブラウザA(利用者A)、FirefoxをブラウザB(利用者B)と呼ぶことにします。
ここでは事前にGoogle Chromeの他にFirefoxのインストールが必要です。Firefoxは次のURLからダウンロードできます。 https://www.mozilla.org/ja/firefox/new/
それでは両方のブラウザでチャット画面( chat.php
)にアクセスしてみましょう。
http://localhost:8000/chat.php
準備ができたのでブラウザA(利用者A)に表示されたチャット画面でHello World!
とメッセージを入力してみましょう。
送信ボタンをクリックすると一覧にメッセージが表示されます。
それからブラウザB(利用者B)でページをリロードすると画面にブラウザA(利用者A)から投稿したデータが表示されるでしょう。
次に不正なスクリプトを入力してみましょう。ここでブラウザB(利用者B)のテキストボックスに次のようなJavaScriptプログラムを入力します。
<script>alert('Script Injection!')</script>
このJavaScriptプログラムはWebブラウザ上にアラートを表示するものです。実際に画面からスクリプトを入力してみましょう。
送信ボタンをクリックすると次のような結果になるでしょう。
実行結果から入力したJavaScriptプログラムがWebブラウザ上で動作していることがわかります。
次にブラウザA(利用者A)に切り替えて、アドレスバーからもう一度URLを入力してチャット画面( chat.php
)にアクセスしてみましょう。
http://localhost:8000/chat.php
そうするとブラウザA(利用者A)上でもブラウザB(利用者B)から入力したJavaScriptプログラムが実行されてしまうのがわかります。今回のJavaScriptプログラムはアラートを表示する単純なものでしたが、プログラムを工夫することでCookieに保存されている情報にアクセスしたり、外部のサイトにデータを送信したりといったことも可能です。
スクリプト挿入攻撃の原因
なぜこのような現象が起きてしまうのか、原因を考えてみましょう。ここではブラウザA(利用者A)とブラウザB(利用者B)の2つが登場しましたが、攻撃者はブラウザB(利用者B)であり、その被害に合うのはブラウザA(利用者A)です。ですが、その原因となる脆弱性はWebアプリケーションの開発元にあります。
今回のチャットプログラム( chat.php
)ではユーザの入力データに対して、特別な措置を取らずに、そのままHTML上に出力している部分に問題があります。具体的には echo
命令で変数を出力している以下の部分です。
<ul>
<?php for ($i=0; $i < count($messages); $i++) { ?>
<li><?php echo $messages[$i]; ?></li>
<?php } ?>
</ul>
このとき利用者からの投稿データは配列変数 $messages
に代入されているので、今回のように実験した場合、プログラムが動作した結果は次のようなHTMLの出力になります。
<ul>
<li>Hello World!</li>
<li><script>alert('Script Injection!')</script>
</li>
</ul>
Webブラウザは上記のようなHTMLを解析することでJavaScriptプログラムを実行します。そのため画面にアラートが表示されることになります。このようにスクリプト挿入攻撃の原因は、ユーザからの入力データに対して特別な措置をとらず、そのままHTMLコンテンツ上に出力しているところにあります。
スクリプト挿入攻撃の対策
それではどのような対策を取れば良いのでしょうか。このようなケースにおいては、ユーザからの入力データにHTML特殊文字が存在する場合は、適切に変換してから出力するようにします。HTML特殊文字とは具体的には以下のようなものです。
他にも
'
シングルクォーテーションを変換('
)することもあります。
HTML特殊文字とはHTML上で特別な役割を持つ文字のことで、タグを構成する <
記号や >
といったものです。これらの文字はタグの一部とみなされてしまうため、Webブラウザ上に文字として <
記号を出力する場合は <
と記述する必要があります。これらのHTML特殊文字を echo
で出力する際に確実に変換しておけば今回のような攻撃を防ぐことができます。
PHPにはこれらのHTML特殊文字を変換するための関数として htmlspecialchars
関数が用意されているので、この関数を使って投稿データを出力するようにチャットプログラム( chat.php
)修正してみましょう。
<?php
$file = "chat.txt";
$messages = file($file, FILE_IGNORE_NEW_LINES);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$message = (string)filter_input(INPUT_POST, "message");
if ($message !== "") {
$messages[] = $message;
file_put_contents($file, $message . PHP_EOL,
FILE_APPEND | LOCK_EX);
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Chat</h3>
<hr>
<form action="chat.php" method="post">
<input type="text" name="message">
<input type="submit" value="send">
</form>
<ul>
<?php for ($i=0; $i < count($messages); $i++) { ?>
<li><?php echo htmlspecialchars($messages[$i]); ?></li>
<?php } ?>
</ul>
</body>
</html>
このプログラムの修正箇所は echo
命令による出力部分です。
<li><?php echo htmlspecialchars($messages[$i]); ?></li>
このように htmlspecialchars
関数の引数に変換対象の文字列を指定するようにします。また htmlspecialchars
関数はデフォルトではシングルクォーテーションは変換しないようになっています。 シングルクォーテーションも変換対象とする場合は、 htmlspecialchars
関数の第2引数に ENT_QUOTES
を指定するようにします。
<li><?php echo htmlspecialchars($messages[$i], ENT_QUOTES); ?></li>
以上でプログラムの修正は完了です。それではブラウザA(利用者A)からもう一度チャット画面( chat.php
)にアクセスしてみましょう。
http://localhost:8000/chat.php
画面にはJavaScriptによるアラートは表示されずに、次のような結果が表示されるでしょう。
また実際にブラウザA(利用者A)において、画面に表示している内容のHTMLソースを確認してみましょう。Chromeの場合は右クリックメニューから「ページのソースを表示」を選択します。
入力されたJavaScriptプログラムは以下のように変換されているのがわかるでしょう。
<li><script>alert('Script Injection!')</script></li>
まとめ
- リクエストパラメータには不正なスクリプトが挿入される可能性がある
- ユーザの入力した内容について、適切な処理をせずにPHPの出力に使ってはいけない
htmlspecialchars
関数を使ってHTMLの特殊文字(<
や>
など)をHTMLエンティティ(実体参照)に変換する