【XSS入門】送られてきた値をそのまま表示すると何が起きるのか
章: 2章
技術タグ: PHP XSS バリデーション フォーム
今回やること
前回は、PHPの $_POST を使って、お問い合わせフォームから送られてきた値を受け取りました。
そして、受け取った値をそのまま画面に表示しました。
今回は、その危険を実際に確認します。
入力欄にHTMLタグやJavaScriptのような文字列を入れて、そのまま表示すると何が起きるのかを体験します。
今回のテーマは、XSSです。
XSS ↓ クロスサイトスクリプティング ↓ ユーザーが送った文字列が、HTMLやJavaScriptとして実行されてしまう危険
目的は、攻撃方法を覚えることではありません。
目的は、ユーザーが送ってきた値をそのまま信用してはいけない理由を、画面上で理解することです。
今回のゴール
今回のゴールは、以下です。
- 送られてきた値をそのまま表示する危険を知る
- HTMLタグを入力した時に画面が変わることを確認する
- JavaScriptのような文字列が実行される可能性を体験する
- XSSの基本イメージを理解する
- htmlspecialchars() でエスケープする理由を理解する
- 出力時エスケープの重要性を説明できるようになる
今回は、あえて危険なコードから始めます。
危険を見てから、防御の意味を理解します。
XSSとは何か
XSSは、クロスサイトスクリプティングの略です。
ざっくり言うと、ユーザーが入力した文字列が、ただの文字ではなくHTMLやJavaScriptとして扱われてしまう問題です。
たとえば、本来はお問い合わせ内容として表示したいだけなのに、入力されたHTMLタグが画面の一部として解釈されてしまうことがあります。
さらに危険な場合、JavaScriptが実行されてしまう可能性もあります。
| 本来やりたいこと | 危険な状態 |
|---|---|
| 入力された文字をそのまま文章として表示する | 入力された文字がHTMLやJavaScriptとして解釈される |
| お問い合わせ内容を確認する | 画面が改ざんされたり、スクリプトが実行されたりする |
つまり、XSSは「表示」の問題です。
受け取ることだけでなく、画面に出す時の扱いが重要になります。
前回のreceive.phpを確認する
前回の receive.php では、フォームから送られてきた値をそのまま表示していました。
<?php
/**
* receive.php
*
* 役割:
* - フォームからPOST送信された値を受け取る
* - 受け取った値を画面に表示する
*
* 注意:
* - 今回は学習のため、あえてエスケープ処理を入れていません。
* - この状態だと、XSSの危険があります。
*/
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$message = $_POST['message'] ?? '';
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>送信内容の確認</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="contact-page">
<h1 class="contact-page__title">送信内容の確認</h1>
<div class="result-box">
<div class="result-box__item">
<p class="result-box__label">お名前</p>
<p class="result-box__value"><?php echo $name; ?></p>
</div>
<div class="result-box__item">
<p class="result-box__label">メールアドレス</p>
<p class="result-box__value"><?php echo $email; ?></p>
</div>
<div class="result-box__item">
<p class="result-box__label">お問い合わせ内容</p>
<p class="result-box__value"><?php echo $message; ?></p>
</div>
<a class="result-box__link" href="index.php">フォームへ戻る</a>
</div>
</main>
</body>
</html>特に危険なのは、次のような出力です。
<?php echo $message; ?>これは、受け取った値をそのままHTMLの中に出しています。
普通の文字列なら問題なく見えます。
しかし、HTMLタグやJavaScriptのような文字列が送られてきた場合、ブラウザがそれをHTMLとして解釈してしまう可能性があります。
まずはHTMLタグを入力してみる
お問い合わせ内容に、次のような文字列を入力して送信してみます。
<h2>これは見出しです</h2>もしエスケープしていない状態なら、ブラウザはこれをただの文字ではなく、HTMLタグとして解釈する可能性があります。
つまり、確認画面で文字が大きな見出しのように表示されることがあります。
ここで大事なのは、次の事実です。
ユーザーが入力した文字列が 画面のHTML構造に影響してしまっている
これは、かなり危険な兆候です。
ただの文字として表示したいのに、HTMLとして解釈されているからです。
JavaScriptのような文字列も試してみる
次に、学習用として次のような文字列を入力してみます。
<script>alert('XSSテスト')</script>環境やブラウザの挙動によっては、この文字列がJavaScriptとして実行され、アラートが表示される可能性があります。
もしアラートが表示された場合、それはユーザーが入力した文字列が、ただの文章ではなくスクリプトとして扱われたということです。
これがXSSの危険です。
この実験は、必ず自分のローカル環境・自分の学習用ファイルで行ってください。
他人のサイトや実サービスに対して試してはいけません。
なぜこれが危険なのか
今回の例では、アラートが出るだけなので軽く見えるかもしれません。
しかし、本質はそこではありません。
問題は、ユーザーが送った文字列が、ブラウザ上でスクリプトとして実行される可能性があることです。
実サービスでXSSが起きると、次のような被害につながる可能性があります。
- 画面表示が改ざんされる
- 意図しない操作が実行される
- ユーザーを偽のページへ誘導される
- セッションや入力情報が狙われる
- 管理画面に危険な文字列が表示される
だから、ユーザーが送ってきた値は、表示する時に必ず安全な形へ変換する必要があります。
原因は「そのまま表示」していること
今回の原因は、受け取った値をそのまま echo していることです。
<?php echo $message; ?>PHPは、文字列の中身が普通の文章なのか、HTMLタグなのか、JavaScriptなのかを自動で判断して守ってくれるわけではありません。
そのため、開発者が出力時に安全な形へ変換する必要があります。
受け取る ↓ そのまま表示する ↓ 危険 受け取る ↓ エスケープして表示する ↓ 安全に近づく
エスケープとは何か
エスケープとは、HTMLとして特別な意味を持つ文字を、ただの文字として表示できる形に変換することです。
たとえば、< や > はHTMLタグに使われる特別な文字です。
これらをそのまま表示すると、ブラウザがタグとして解釈する可能性があります。
そこで、次のような形に変換します。
| 元の文字 | 変換後の例 |
|---|---|
| < | < |
| > | > |
| ‘ | ' |
| “ | " |
| & | & |
こうすることで、ブラウザはHTMLタグとしてではなく、ただの文字として表示しやすくなります。
htmlspecialchars()で防御する
PHPでは、HTML出力時のエスケープに htmlspecialchars() を使います。
今回のコードでは、エスケープ用の関数を用意して使います。
【ここに Highlighting Code Block:安全なreceive.php(PHP)】
<?php
/**
* receive.php
*
* 役割:
* - フォームからPOST送信された値を受け取る
* - 出力時にhtmlspecialchars()でエスケープする
* - 受け取った値を安全に画面へ表示する
*/
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$message = $_POST['message'] ?? '';
/**
* HTML出力用に文字列をエスケープする
*
* @param string $value 画面に表示したい文字列
* @return string エスケープ済みの文字列
*/
function escape_html($value)
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>送信内容の確認</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="contact-page">
<h1 class="contact-page__title">送信内容の確認</h1>
<div class="result-box">
<div class="result-box__item">
<p class="result-box__label">お名前</p>
<p class="result-box__value"><?php echo escape_html($name); ?></p>
</div>
<div class="result-box__item">
<p class="result-box__label">メールアドレス</p>
<p class="result-box__value"><?php echo escape_html($email); ?></p>
</div>
<div class="result-box__item">
<p class="result-box__label">お問い合わせ内容</p>
<p class="result-box__value"><?php echo escape_html($message); ?></p>
</div>
<a class="result-box__link" href="index.php">フォームへ戻る</a>
</div>
</main>
</body>
</html>この中で重要なのは、次の関数です。
function escape_html($value)
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
htmlspecialchars() は、HTMLとして特別な意味を持つ文字を安全な表示用の文字に変換してくれます。
ENT_QUOTES を指定すると、シングルクォートとダブルクォートも変換対象になります。
UTF-8 は文字コードの指定です。
出力部分を安全にする
危険だった出力は、次のような形でした。
<?php echo $message; ?>これを、エスケープ関数を通す形に変えます。
<?php echo escape_html($message); ?>このように、画面へ表示する直前にエスケープします。
入力時ではなく、出力時にエスケープするのが基本です。
DBに保存する値 ↓ 基本的には元の意味を保つ 画面に表示する時 ↓ HTMLとして危険な文字をエスケープする
今回はDB保存までは行っていませんが、この考え方は後の章でも重要になります。
修正後にもう一度試す
安全な receive.php に差し替えたら、もう一度同じ文字列を送信してみます。
<script>alert('XSSテスト')</script>修正前は、HTMLやJavaScriptとして解釈される可能性がありました。
修正後は、次のようにただの文字として表示されるはずです。
<script>alert('XSSテスト')</script>アラートが実行されず、文字として表示されれば、防御が効いています。
これがエスケープの効果です。
curlでも確認できる
ブラウザのフォームだけでなく、curlからも同じような文字列をPOSTできます。
たとえば、Git Bashから以下のように送信できます。
curl -X POST http://contact-form-app.test/receive.php \
-d "name=Leon" \
-d "email=leon@example.com" \
-d "message=<script>alert('XSSテスト')</script>" このように、フォーム画面を通らなくても、サーバーへ値は送れます。
だからこそ、画面側だけでなく、PHP側で受け取った値を安全に扱う必要があります。
curlは、ブラウザ側の入力補助を完全に通らずに送れるので、サーバー側チェックの重要性を理解するのにかなり役立ちます。
LG流:受け取る時より、出す時が危ない
今回のポイントは、受け取った瞬間だけではありません。
危険が表に出るのは、画面に表示する時です。
ユーザーから受け取る ↓ そのまま保存する ↓ あとで管理画面に表示する ↓ そこでXSSが発火する
このように、受け取った時には何も起きなくても、後から表示した時に問題になることがあります。
だから、出力時エスケープが重要です。
LG流で言うなら、こうです。
入力値は信用しない 出力時は必ずエスケープする
サーバー側バリデーションとエスケープは別物
ここで、バリデーションとエスケープの違いも整理します。
| 処理 | 目的 | 例 |
|---|---|---|
| バリデーション | 入力ルールに合っているか確認する | 空欄チェック、メール形式チェック、文字数チェック |
| エスケープ | 表示時にHTMLとして危険な文字を無害化する | htmlspecialchars() |
どちらも大事ですが、役割が違います。
空欄チェックをしていても、XSS対策にはなりません。
メール形式をチェックしていても、お問い合わせ本文にHTMLタグが入る可能性はあります。
だから、バリデーションとエスケープは両方必要です。
今回わかったこと
今回は、送られてきた値をそのまま表示する危険を体験しました。
学んだことは以下です。
- ユーザーが送ってきた値は信用してはいけない
- 受け取った値をそのままechoすると危険
- HTMLタグが画面構造に影響する可能性がある
- JavaScriptのような文字列が実行される可能性がある
- XSSは表示時に起きる危険である
- htmlspecialchars() でHTML出力用にエスケープできる
- 入力チェックと出力エスケープは別物である
- フォーム画面を通らなくてもcurlで直接POSTできる
フォーム処理では、受け取ることよりも、その後どう扱うかが重要です。
ユーザーが送ってきた値を信用しない。
画面に出す時は必ずエスケープする。
これは、今後ずっと使う基本です。
次回予告
次回は、今回学んだXSS対策を踏まえて、PHP側の入力チェックをもう少し整理します。
空欄チェック、メールアドレス形式チェック、エラーメッセージの表示をPHP側で行い、JavaScriptに頼らないフォーム処理へ進みます。
次回のテーマは、以下です。
【PHPバリデーション入門】サーバー側で入力チェックしてエラーを表示する
ここから、フォームは少しずつ実務の形に近づいていきます。