はじめに
Webアプリケーションを開発する上で、セキュリティ対策は避けて通れない重要な課題です。中でもXSS(クロスサイトスクリプティング)は、長年にわたって多くのWebサイトで発生している代表的な脆弱性の一つです。
本記事では、XSSの基本的な仕組みと、実践的な対策方法について解説します。
XSSとは
XSS(Cross-Site Scripting)は、Webアプリケーションの脆弱性を突いて、悪意のあるスクリプトを他のユーザーのブラウザ上で実行させる攻撃手法です。
XSS攻撃の仕組み
XSS攻撃は、以下のような流れで実行されます。
攻撃者は、掲示板のコメント欄や検索フォームなどに、JavaScriptコードを含む文字列を入力します。アプリケーション側で適切な処理がされていない場合、そのスクリプトがそのままHTMLとして出力され、被害者のブラウザ上で実行されてしまいます。
XSS攻撃による被害例
XSS攻撃が成功すると、以下のような被害が発生する可能性があります。
- セッションIDやクッキーの盗難によるアカウント乗っ取り
- 偽のログインフォームを表示させてパスワードを盗む
- ページの内容を改ざんして偽の情報を表示する
- 外部の悪意あるサイトへ自動的にリダイレクトさせる
XSS対策の全体像
XSS対策は、「必須対策」と「保険的対策」の二段構えで考えることが重要です。
必須対策は脆弱性そのものを防ぐための基本的な対策であり、保険的対策は万が一の際に被害を最小限に抑えるための追加の防御層です。
必須対策
HTMLの要素内容のエスケープ
ユーザーから受け取った文字列をHTML内に出力する際は、必ずエスケープ処理を行います。エスケープとは、特殊な意味を持つ文字を無害な文字列に変換することです。
PHPではhtmlspecialchars関数を使用します。
<?php
// ユーザーからの入力
$user_input = $_POST['comment'];
// エスケープして出力
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
?>
この処理により、以下のような変換が行われます。
| 変換前 | 変換後 |
|---|---|
< |
< |
> |
> |
" |
" |
' |
' |
& |
& |
例えば、攻撃者が<script>alert('XSS')</script>という文字列を入力した場合、エスケープ後は<script>alert('XSS')</script>となり、ブラウザ上では単なる文字列として表示されるだけで、スクリプトとして実行されることはありません。
属性値のエスケープ
HTMLタグの属性値に動的な値を埋め込む場合も、同様にエスケープが必要です。さらに、属性値は必ずダブルクォート"で囲むようにします。
<?php
$user_name = $_POST['name'];
?>
<!-- 正しい例 -->
<input type="text" value="<?php echo htmlspecialchars($user_name, ENT_QUOTES, 'UTF-8'); ?>">
<!-- 危険な例(クォートなし) -->
<input type="text" value=<?php echo $user_name; ?>>
クォートで囲まない場合、スペースを使って属性を閉じて新しい属性を追加する攻撃が可能になってしまいます。
文字エンコーディングの明示
HTTPレスポンスヘッダで文字エンコーディングを明示することで、ブラウザが誤った文字コードで解釈してセキュリティホールが生まれるのを防ぎます。
PHPでの設定例:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
HTMLのmetaタグでも指定します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ページタイトル</title>
</head>
文字エンコーディングを明示しないと、ブラウザが自動判定を行い、特定のバイト列がスクリプトとして解釈されてしまう可能性があります。
保険的対策
必須対策を実施した上で、さらに以下の保険的対策を行うことで、多層防御を実現します。
X-XSS-Protectionヘッダの使用
ブラウザに組み込まれたXSSフィルタを有効化するヘッダです。
<?php
header('X-XSS-Protection: 1; mode=block');
?>
ただし、このヘッダは古い仕様であり、現在では後述のCSP(Content Security Policy)の使用が推奨されています。モダンブラウザの多くは、このヘッダを非推奨としています。
入力値の検証
ユーザーからの入力が期待する形式かどうかを検証します。これは出力時のエスケープとは別に、入力段階でチェックを行うものです。
<?php
// 数値のみを期待する場合
if (!is_numeric($_POST['age'])) {
die('不正な入力です');
}
// メールアドレス形式のチェック
if (!filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
die('メールアドレスの形式が正しくありません');
}
?>
入力値検証は「ホワイトリスト方式」(許可するものを定義する)で行うのが基本です。「ブラックリスト方式」(禁止するものを定義する)は、想定外のパターンを見逃すリスクがあります。
クッキーへのHttpOnly属性付与
クッキーにHttpOnly属性を付けることで、JavaScriptからクッキーを読み取れないようにします。これにより、XSS攻撃でセッションIDが盗まれるリスクを大幅に減らせます。
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
?>
HttpOnly属性が設定されたクッキーは、document.cookieでアクセスできなくなります。
// HttpOnly属性が設定されている場合、セッションIDは見えない
console.log(document.cookie); // セッションIDは表示されない
TRACEメソッドの無効化
HTTPのTRACEメソッドを悪用したXST(Cross-Site Tracing)攻撃を防ぐため、Webサーバーの設定でTRACEメソッドを無効化します。
Apacheの設定例:
TraceEnable off
Nginxの設定例:
if ($request_method = TRACE) {
return 405;
}
TRACEメソッドは、リクエストをそのまま返すデバッグ用の機能ですが、HttpOnly属性を回避してクッキーを盗む攻撃に使われる可能性があるため、本番環境では無効化しておくことが推奨されます。
対策の優先順位と考え方
XSS対策を実装する際の優先順位は以下の通りです。
- 出力時のエスケープ処理(最優先): すべての動的な出力に対して必ず実施
- 文字エンコーディングの明示: レスポンスヘッダとmetaタグで指定
- HttpOnly属性の設定: セッション管理において必須
- 入力値の検証: データの整合性チェックとして実施
- その他のセキュリティヘッダ: 多層防御の一環として設定
重要なのは、「入力値の検証だけではXSSは防げない」という点です。検証をすり抜けるパターンは無数に存在するため、最終的な防御は出力時のエスケープで行う必要があります。
また、最近ではCSP(Content Security Policy)の導入も効果的です。CSPを使うと、インラインスクリプトの実行を禁止したり、スクリプトの読み込み元を制限したりできます。
<?php
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com");
?>
まとめ
XSS対策の基本は、「信頼できない入力を適切にエスケープして出力する」ことです。これを確実に実施した上で、HttpOnly属性の設定や入力値検証などの保険的対策を組み合わせることで、より強固なセキュリティを実現できます。
重要なポイントを整理します。
- 必須対策としてエスケープ処理を必ず実施する
- 文字エンコーディングを明示する
- HttpOnly属性でクッキーを保護する
- 入力値検証は補助的な対策として位置づける
- 多層防御の考え方で複数の対策を組み合わせる
セキュリティ対策は一度実装したら終わりではなく、継続的に見直しと改善が必要です。新しい攻撃手法も日々生まれているため、最新の情報をキャッチアップしながら、適切な対策を講じていきましょう。