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.phpmethod 属性には 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特殊文字とは具体的には以下のようなものです。


他にも ' シングルクォーテーションを変換( &#039 )することもあります。

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エンティティ(実体参照)に変換する