セキュアなCookie設定ガイド|HttpOnly・Secure・SameSiteの使い分けとCSRF対策

Cookie セキュリティ CSRF XSS Web
結論

セッション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.cookieXMLHttpRequest / 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の銀の弾ではない

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だけが受理される。

  1. Secure 属性が付いている
  2. Path=/ である
  3. Domain 属性が存在しない(現在のホストに完全固定)
Set-Cookie: __Host-session=9e4f1a8c2d7b3e6f; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600

サブドメイン間での意図しないCookie共有を防ぎたいときに使う。例えば app.example.comapi.example.com でセッションを共有したい場合は Domain=.example.com が必要だが、共有したくないセッションは __Host- でホストにロックする。

Domain を誤って付けるとブラウザはCookie全体を拒否する——サイレントにログインできなくなるので、DevToolsでの確認が必須だ。

__Secure- プレフィックス

名前が __Secure- で始まるCookieは Secure 属性が必須だが、PathDomain の制約は __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 のサードパーティCookieStorage Access API、第一者コンテキストへのリダイレクト認証
クロスドメイン計測・広告サードパーティCookiePrivacy 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・エスケープ)が最下層になる。

多層防御の組み合わせ

対策主に防ぐ脅威
1SameSite=Lax/StrictクロスサイトCSRF
2CSRFトークン(Synchronizer / Double Submit)SameSiteの穴・None環境
3HttpOnly + SecureCookie窃取・平文送信
4CSP + 出力エスケープXSS注入そのもの
5__Host- プレフィックスサブドメイン間の意図しない共有

状態変更リクエストにはPOST/PUT/PATCH/DELETEに限定し、GETに副作用を載せないことも、Cookie属性と並ぶ基本中の基本だ。

Express・cookie-sessionでの実装例

Node.jsのExpressでセッション管理する場合、express-sessioncookie-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セッション(サーバーにセッションストアを持たない)向け。

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分
});

フレームワークを使わない場合の参考例。

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とヘッダーの一致を検証する。

実務チェックリストと検証手順

1

セッション・CSRFトークン・UI設定Cookieを分類し、機密度の高いものから HttpOnly + Secure を付与する

2

SameSite は基本 Lax。OAuth / iframe / 決済などクロスサイトが必要なCookieだけ None + Secure にする

3

ホスト固定が必要なセッションは __Host- プレフィックス + Path=/ + Domain なし を適用する

4

状態変更は POST/PUT/PATCH/DELETE に限定し、CSRFトークン検証を SameSite と併用する

5

Chrome DevTools → Application → Cookies で属性を目視確認する

6

クロスサイト POST・iframe 埋め込み・外部リンクからの遷移をステージング(HTTPS)で通しテストする

7

CSP と出力エスケープで XSS 注入を防ぎ、Cookie属性だけに依存しない多層防御を確認する

DevToolsでの確認ポイント

  1. Application → Cookies で各Cookieの HttpOnlySecureSameSitePathExpires を確認
  2. Network タブでリクエストの Cookie ヘッダーが、想定どおりの遷移(同一サイト/クロスサイト)で付与・非付与になるか検証
  3. 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分の確認が、インシデント対応の数日分に相当する。