セキュアなCookie設定ガイド|HttpOnly・Secure・SameSiteの使い分けとCSRF対策
セッションCookieはHttpOnly + Secure + SameSite=Laxをデフォルトにする。クロスサイト連携が必要なときだけSameSite=None(Secure必須)。ホスト固定が必要ならプレフィックスを使う。Cookie属性はCSRF・XSS対策の第一層であり、トークン検証・CSP・入力サニタイズとの多層防御が実務の定石だ。
Cookieが攻撃面になる理由
Cookieはブラウザが同一オリジンへのリクエストに自動付与する仕組みのため、攻撃者にとって高価値な標的になる。主な脅威は2つ——XSS(クロスサイトスクリプティング)による窃取と、CSRF(クロスサイトリクエストフォージェリ)によるなりすまし操作だ。
XSSが成立すると、悪意あるスクリプトが document.cookie からセッションIDを読み取り、外部サーバーへ送信できる(HttpOnlyが無い場合)。さらに深刻なのは、Cookieを盗まなくてもログイン済みブラウザ上でAPIを直接呼び出すことで、送金・設定変更・データ削除を実行できる点だ。HttpOnlyは「Cookie文字列の窃取」を防ぐが、ブラウザが保持する認証状態そのものは悪用されうる。
CSRFは逆の経路だ。ユーザーが bank.example にログインした状態で攻撃者のページを開くと、<form method="POST"> や fetch({ credentials: 'include' }) 経由で、ブラウザが知らないうちにセッションCookieを付けて状態変更リクエストを送る。サーバーがOriginやトークンを検証していなければ、本人の操作として処理されてしまう。
Cookieの属性(HttpOnly・Secure・SameSite)は、この2つの攻撃ベクトルの送信経路と読み取り経路を絞り込むための基盤だ。ただし属性だけでは完結しない。後述するCSRFトークンやCSPとの組み合わせが前提になる。
HttpOnly属性——XSSによるセッション窃取を抑える
HttpOnly を付けたCookieは、JavaScriptの document.cookie や XMLHttpRequest / fetch のレスポンスヘッダーから読み書きできない。XSSで注入されたスクリプトがセッションIDを文字列として抜く典型パターンの第一防御線になる。
Set-Cookie: session=eyJhbGciOiJIUzI1NiJ9...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
HttpOnlyを付けるべき値
- セッションID・リフレッシュトークン
- サーバーだけが検証するCSRFトークン(Double Submit方式でCookieに載せる場合)
- 認可判断に使うサーバーサイドの識別子
HttpOnlyにできない/付けない値
- フロントエンドが参照するUI設定(テーマ・言語)
- Double Submit CookieでJavaScriptがヘッダーに載せるCSRFトークン(Cookie自体はHttpOnlyにできない設計もあるが、セキュリティトレードオフに注意)
HttpOnlyがあっても、XSSされたページ上では fetch('/api/transfer', { method: 'POST', credentials: 'include', body: ... }) のように認証済みAPIを直接叩ける。Cookie窃取は防げても「ブラウザに乗っ取られた操作」は止められない。CSPでインラインスクリプトを封じ、出力エスケープで注入自体を防ぐことが本質的な対策だ。
Secure属性——HTTPS限定送信と開発環境の落とし穴
Secure 属性があるCookieは、暗号化された接続(HTTPS)へのリクエストにのみブラウザが付与する。公共Wi-Fi上のMITM(中間者攻撃)で、平文HTTPレスポンスにセッションが流れるリスクを下げる。
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
本番環境ではセッション系Cookieに Secure は必須。開発環境の http://localhost は多くのブラウザでSecure Cookieを例外扱いするが、本番ドメインとは挙動がずれる。ステージングもHTTPSにしておくと、リリース前の検証が本番と一致する。
HSTS(HTTP Strict Transport Security) と併用すると効果が増す。HSTSはブラウザに「このドメインへは今後HTTPでアクセスしない」と記憶させ、最初のHTTPアクセス以前のダウングレード攻撃も抑えられる。
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Secure は「HTTPSリクエストにだけCookieを付ける」、HSTS は「そもそもHTTPSを強制する」——役割が異なるため、本番ではHTTPSリダイレクト + HSTS + Secure Cookieの三層を推奨する。
SameSite属性——Strict・Lax・Noneの実際のSet-Cookie例
SameSite は、別サイト(クロスサイト)からのリクエストにCookieを付けるかを制御する。CSRF対策の要であり、値によって送信条件が大きく変わる。
| SameSite値 | 挙動と用途 |
|---|---|
| Strict | クロスサイトからのリクエストでは一切送信しない。最も厳格。外部サイトのリンクを踏んで初回アクセスしたとき、ログイン状態が切れて見えることがある |
| Lax(推奨デフォルト) | トップレベルナビゲーション(リンククリック、location変更)のGETでは送信。クロスサイトのフォームPOST・iframe・fetchには原則付かない。一般Webアプリのセッションに最適 |
| None | クロスサイトでも送信する。Secure属性が必須(仕様上、Noneかつ非SecureのCookieはブラウザが拒否)。OAuth、決済iframe、サードパーティ埋め込み向け |
SameSite=Strict
外部サイトからのリンクを踏んだ直後のGETでもCookieが送られない。管理画面や金融系の高セキュリティ領域向け。
Set-Cookie: admin_session=7f3a9c2e1b8d4f6a; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=1800
SameSite=Lax
多くのWebアプリケーションのセッションCookieのデフォルト候補。メール内リンクからの遷移ではログイン状態が維持され、クロスサイトPOST型CSRFはブロックされる。
Set-Cookie: session=eyJzdWIiOiJ1c2VyMTIzIn0; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=86400
SameSite=None
クロスサイトのiframe内や、OAuthコールバック、決済ウィジェットなど、他オリジン起点のリクエストにCookieが必要な場合に使う。Chrome・Firefox・Safariすべてで NoneにはSecureが必須。
Set-Cookie: oauth_state=cf9e2a1b7d3c8e5f; Path=/; HttpOnly; Secure; SameSite=None; Max-Age=600
Set-Cookie: embed_session=4a8f2c9e1d7b3a6f; Path=/; HttpOnly; Secure; SameSite=None; Max-Age=3600
SameSite=None を付け忘れると、モダンブラウザではデフォルトが Lax 相当として扱われ、決済iframeやSSO連携が突然動かなくなる典型事故が起きる。リリース前にクロスサイトの動線を必ずステージングで通しテストすること。
__Host- と __Secure- プレフィックスの厳格ルール
Cookie名を特定のプレフィックスで始めると、ブラウザが追加の受理条件を強制する。サーバー側の設定ミスをブラウザが拒否してくれるため、セッション固定化に有効だ。
__Host- プレフィックス
名前が __Host- で始まるCookieは、次の3条件をすべて満たすSet-Cookieだけが受理される。
Secure属性が付いているPath=/であるDomain属性が存在しない(現在のホストに完全固定)
Set-Cookie: __Host-session=9e4f1a8c2d7b3e6f; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
サブドメイン間での意図しないCookie共有を防ぎたいときに使う。例えば app.example.com と api.example.com でセッションを共有したい場合は Domain=.example.com が必要だが、共有したくないセッションは __Host- でホストにロックする。
Domain を誤って付けるとブラウザはCookie全体を拒否する——サイレントにログインできなくなるので、DevToolsでの確認が必須だ。
__Secure- プレフィックス
名前が __Secure- で始まるCookieは Secure 属性が必須だが、Path や Domain の制約は __Host- より緩い。
Set-Cookie: __Secure-analytics_id=a7f3c9e2b1d8; Path=/; Secure; SameSite=Lax; Max-Age=31536000
Set-Cookie: __Secure-shared=5c8e1f4a9b2d; Domain=.example.com; Path=/; Secure; HttpOnly; SameSite=Lax
サブドメイン共有が必要で、かつSecureを強制したい識別子に向く。セッションのホスト完全固定が目的なら __Host-、Secure強制だけでよいなら __Secure- を選ぶ。
| プレフィックス | 必須条件 |
|---|---|
| (なし) | 特別な制約なし(各属性は個別に指定) |
| __Secure- | Secure必須。Path・Domainは自由 |
| __Host- | Secure必須 + Path=/ + Domain属性なし |
ChromeサードパーティCookie廃止と今後の設計
Google ChromeはサードパーティCookie(Third-Party Cookie)の段階的廃止を進めている。2024年から一部ユーザーへのテストが始まり、業界全体で「クロスサイトCookie前提」の設計が見直されている。
サードパーティCookieとは、ユーザーが閲覧しているページのドメイン(第一者)と異なるドメインが Set-Cookie したCookieが、iframeやクロスサイトリクエストで送信される状況を指す。広告トラッキングだけでなく、埋め込み決済・SaaSウィジェット・クロスドメイン認証にも影響する。
廃止がアプリに与える影響
| シナリオ | 従来の依存 | 廃止後の方向性 |
|---|---|---|
| 別ドメインiframe内のセッション | SameSite=None; Secure のサードパーティCookie | Storage Access API、第一者コンテキストへのリダイレクト認証 |
| クロスドメイン計測・広告 | サードパーティCookie | Privacy Sandbox(Topics API等)、サーバーサイド計測 |
| OAuth / OIDC | コールバック先ドメインのCookie | 第一者Cookie + state/nonce 検証(従来通りだがiframe内OAuthは要再設計) |
SameSite=None は「クロスサイト送信を許可する」属性であり、サードパーティCookie廃止後も第一者コンテキストでは引き続き有効だ。ただし、他サイトのiframe内で自社Cookieを読む設計は、Storage Access APIやCHIPS(Partitioned Cookie)などの新APIへの移行を検討する必要がある。
Set-Cookie: __Secure-session=abc; Path=/; Secure; HttpOnly; SameSite=None; Partitioned
Partitioned 属性(CHIPS)は、サードパーティコンテキストごとにCookieをパーティション分割し、トラッキング用途を抑えつつ埋め込み用途を維持する試みだ。対応ブラウザと要件を確認したうえで採用を判断する。
実務での指針: 新規開発ではサードパーティCookie前提の認証・セッション設計を避け、第一者ドメインで完結する認証フロー(リダイレクトベースOAuth、BFFパターン)を優先する。
CSRFとXSSの相互作用——Cookieだけでは足りない理由
Cookie属性はCSRFとXSSそれぞれに部分的な効果しかない。2つの攻撃が組み合わさると、単一属性への過信が破綻する。
SameSiteが守る範囲と穴
SameSite=Lax はクロスサイトのフォームPOSTにセッションCookieを付けないため、古典的なCSRFの多くを防げる。ただし次のケースでは不十分だ。
- 同一サイト内XSS: 攻撃スクリプトが同一オリジンで動くため、SameSiteの制限はかからない。
fetchで状態変更APIを叩かれる - GETに副作用があるエンドポイント: LaxはトップレベルGETにCookieを付ける。
GET /delete?id=1型はCSRFの温床 - SameSite=Noneが必要な連携: 決済iframe内ではクロスサイトCookieが送られるため、CSRFトークン検証が必須
- 古いUser-Agent・一部WebView: SameSite未対応環境では従来どおりCookieが送られる
HttpOnlyとXSSの関係
HttpOnlyは document.cookie からの窃取を防ぐが、XSSが存在する限りブラウザの認証状態を悪用した操作は可能だ。攻撃者のスクリプトは以下を実行できる。
// HttpOnlyでも同一オリジンAPIは叩ける
await fetch('/api/user/email', {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: '[email protected]' }),
});
CSRFトークンをSynchronizer Token方式で実装していても、XSSがあればページ内のトークン値を読み取って正当なリクエストとして送れる。つまり「CSRFトークンはXSSがあれば無力化される」——Cookie属性とCSRFトークンは互いを補完するが、XSS対策(CSP・エスケープ)が最下層になる。
多層防御の組み合わせ
| 層 | 対策 | 主に防ぐ脅威 |
|---|---|---|
| 1 | SameSite=Lax/Strict | クロスサイトCSRF |
| 2 | CSRFトークン(Synchronizer / Double Submit) | SameSiteの穴・None環境 |
| 3 | HttpOnly + Secure | Cookie窃取・平文送信 |
| 4 | CSP + 出力エスケープ | XSS注入そのもの |
| 5 | __Host- プレフィックス | サブドメイン間の意図しない共有 |
状態変更リクエストにはPOST/PUT/PATCH/DELETEに限定し、GETに副作用を載せないことも、Cookie属性と並ぶ基本中の基本だ。
Express・cookie-sessionでの実装例
Node.jsのExpressでセッション管理する場合、express-session や cookie-session を使うのが一般的だ。いずれも cookie オプションで属性を明示する——デフォルト値に頼らないことが重要だ。
express-session の設定
import session from 'express-session';
app.use(session({
name: '__Host-session', // __Host- を使う場合は下記 cookie 制約とセット
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', // 'strict' | 'lax' | 'none'
path: '/',
maxAge: 60 * 60 * 1000, // 1時間(ミリ秒)
// domain: undefined, // __Host- 使用時は domain を指定しない
},
}));
本番では secure: true を固定し、trust proxy をリバースプロキシ背後で有効にする。
app.set('trust proxy', 1); // nginx / ALB 背後で secure cookie を正しく判定
cookie-session の設定
ステートレスな署名付きCookieセッション(サーバーにセッションストアを持たない)向け。
import cookieSession from 'cookie-session';
app.use(cookieSession({
name: '__Host-app-session',
keys: [process.env.COOKIE_SECRET_1, process.env.COOKIE_SECRET_2],
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
// signed: true はデフォルト。改ざん検知に有効
}));
OAuth連携などクロスサイトCookieが必要な場合は、該当ルートだけ sameSite: 'none' に切り替える。
// OAuth state 用の短命Cookie(コールバック検証後に削除)
res.cookie('oauth_state', stateToken, {
httpOnly: true,
secure: true, // SameSite=None では secure: true が必須
sameSite: 'none',
path: '/auth/callback',
maxAge: 10 * 60 * 1000, // 10分
});
レスポンスで直接 Set-Cookie を書く場合
フレームワークを使わない場合の参考例。
res.setHeader('Set-Cookie', [
'session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600',
'csrf_token=def456; Path=/; Secure; SameSite=Strict; Max-Age=3600',
]);
csrf_token をDouble Submit方式で使う場合、JavaScriptから読む必要があるため HttpOnly は付けない——代わりに値自体を推測困難なランダム文字列にし、サーバーでCookieとヘッダーの一致を検証する。
実務チェックリストと検証手順
セッション・CSRFトークン・UI設定Cookieを分類し、機密度の高いものから HttpOnly + Secure を付与する
SameSite は基本 Lax。OAuth / iframe / 決済などクロスサイトが必要なCookieだけ None + Secure にする
ホスト固定が必要なセッションは __Host- プレフィックス + Path=/ + Domain なし を適用する
状態変更は POST/PUT/PATCH/DELETE に限定し、CSRFトークン検証を SameSite と併用する
Chrome DevTools → Application → Cookies で属性を目視確認する
クロスサイト POST・iframe 埋め込み・外部リンクからの遷移をステージング(HTTPS)で通しテストする
CSP と出力エスケープで XSS 注入を防ぎ、Cookie属性だけに依存しない多層防御を確認する
DevToolsでの確認ポイント
- Application → Cookies で各Cookieの
HttpOnly・Secure・SameSite・Path・Expiresを確認 - Network タブでリクエストの
Cookieヘッダーが、想定どおりの遷移(同一サイト/クロスサイト)で付与・非付与になるか検証 SameSite=NoneのCookieがSecureなしで拒否されていないか確認(Issuesタブに警告が出る)
よくある事故と対処
| 症状 | 想定原因 | 対処 |
|---|---|---|
| 本番だけログインできない | secure: true だがHTTPで配信/trust proxy 未設定 | HTTPS化と trust proxy 設定 |
| 決済iframeが突然動かない | SameSite=None 未設定または Secure 欠落 | 両方を明示的に付与 |
__Host- Cookieが保存されない | Domain を付けている/Path が / でない | 制約をすべて満たすよう修正 |
| サブドメイン間でセッションが共有されない | __Host- を使っている | 共有が必要なら通常名 + Domain=.example.com |
関連記事
この記事は セキュリティ テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。
| トピック | 記事 |
|---|---|
| XSS攻撃と防御の完全ガイド | XSS攻撃と防御の完全ガイド|反射型・蓄積型・DOM型の対策 |
| CSRF対策の実装ガイド | CSRF対策の実装ガイド|SameSite・Synchronizer Token・Laravel/Expressコード |
| Double Submit Cookie完全実装 | Double Submit Cookie完全実装|Synchronizer Token比較・SPA+JWT・サブドメイン攻撃 |
| Content Security Policy (CSP) 設定ガイド | Content Security Policy (CSP) 設定ガイド|XSS・クリックジャッキング対策 |
| CSP Report-Only 段階導入ガイド | CSP Report-Only 段階導入ガイド|本番を壊さずポリシーを検証する |
| セキュリティヘッダー一覧と設定ガイド | セキュリティヘッダー一覧と設定ガイド|CSP・HSTS・Referrer-Policy・Permissions-Policy |
| CORSエラーの対処法 | CORSエラーの対処法|「Access-Control-Allow-Origin」で解決する |
| CORSプリフライトリクエスト | CORSプリフライトリクエスト|OPTIONSが飛ぶ条件と正しい応答 |
| ローカル開発でCORSエラーが出る原因と3つの回避策 | ローカル開発でCORSエラーが出る原因と3つの回避策 |
| OAuth2 PKCE SPA実装ガイド | OAuth2 PKCE SPA実装ガイド|Authorization Code + S256 + BFFパターン |
| Passkeys・WebAuthn実装の基礎 | Passkeys・WebAuthn実装の基礎|FIDO2・Resident Key・@simplewebauthn/server |
| JWTのデバッグ方法 | JWTのデバッグ方法|トークンの中身を安全に確認する |
まとめ
セキュアなCookie設計は、単一属性のチェックリストではなく脅威モデルに沿った多層防御だ。HttpOnly でXSSによるCookie文字列の窃取を抑え、Secure とHSTSで平文送信を防ぎ、SameSite でクロスサイトCSRFの入口を狭め、必要に応じて __Host- / __Secure- でブラウザレベルの強制力を加える。
ChromeのサードパーティCookie廃止は、クロスサイトCookie前提の設計を長期的に見直す契機になる。新規では第一者コンテキストで完結する認証フローを優先し、埋め込みが必要な場合は Storage Access API や Partitioned Cookie への対応を検討する。
最後に、属性を設定したらDevToolsでの目視確認とクロスサイト動線の実機テストを必ず行うこと。Cookieの属性ミスはサイレントに効き、ステージングと本番のドメイン差異でだけ発覚する——リリース前の15分の確認が、インシデント対応の数日分に相当する。