コンテンツにスキップ

Cookie とセッション

Cookie とセッション — RFC 6265

  • Cookie は「状態を運ぶ仕組み」でしかなく、認証そのものではない。 認証状態はサーバー側のセッションが持ち、Cookie はその識別子を往復させるだけ。
  • Cookie の安全性は属性で決まるSecure / HttpOnly / SameSite / __Host- プレフィックスをどう組むかがそのまま防御力になる。
  • BasicDigest で「できない」とされたこと(ログアウト、パスワード再設定、2 要素認証)が、セッションを持つとすべて可能になる。だから一般利用者向けはこの方式になる。

ここを混ぜると以降がすべてぼやける。Cookie は運搬手段、セッションは状態

flowchart TB
    subgraph CLIENT["ブラウザ側"]
        direction TB
        C["Cookie ストア<br/>id=ilphHgCl...pRA"]
    end

    subgraph SERVER["サーバー側"]
        direction TB
        S["セッションストア<br/>id をキーに状態を引く"]
        D["セッションの中身<br/>user_id / 権限 / ログイン時刻"]
        S --> D
    end

    C -->|"Cookie ヘッダで毎回送る"| S
    S -->|"Set-Cookie で発行・更新"| C

    style C fill:#4054b2,color:#fff
    style D fill:#1f5c33,color:#fff
  • Cookie に入っているのは「引き換え券」だけ。 権限や利用者名を Cookie 本体に入れてはいけない(クライアントが書き換えられる)
  • 失効はサーバー側でできる。 セッションストアからレコードを消せば、Cookie が残っていても無効になる。これが Basic / Digest との決定的な差
  • Cookie は認証以外にも使われる(言語設定、同意状態、計測)。セッション Cookie と機能 Cookie を混同しない
sequenceDiagram
    autonumber
    participant B as ブラウザ
    participant S as サーバー

    B->>S: GET /login
    S-->>B: ログインフォーム
    Note over B,S: この時点でセッションを作るなら<br/>ログイン後に必ず再発行する
    B->>S: POST /login (id と password)
    S->>S: パスワードを Argon2 で検証
    S->>S: セッション ID を新規発行<br/>古い ID は破棄する
    S-->>B: 302 + Set-Cookie<br/>__Host-id=... Secure HttpOnly<br/>SameSite=Lax Path=/
    B->>S: GET /mypage<br/>Cookie: __Host-id=...
    S->>S: ID でセッションを引き<br/>有効期限を確認
    S-->>B: 200 OK (本人向けの内容)
    B->>S: POST /logout
    S->>S: セッションストアから削除
    S-->>B: 302 + Set-Cookie<br/>__Host-id 空値 Max-Age=0
    Note over B,S: サーバー側の破棄が本体。<br/>Cookie の削除は後始末

押さえどころは 2 つ。

  1. ログイン成功時にセッション ID を作り直す。 作り直さないと後述のセッション固定攻撃が成立する。
  2. ログアウトはサーバー側の破棄が本体。 Set-Cookie で消すだけでは、攻撃者が既に盗んだ ID をそのまま使える。
Set-Cookie: __Host-id=ilphHgClQwc-SUQHMdRx0J4X161gc1iVTOzhQyUmpRA; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800

Set-Cookie の各属性が何を決めるか

属性 何を決めるか セッション Cookie での既定解
Expires 絶対時刻での失効 使わない(Max-Age を使う)
Max-Age 秒数での失効。Expires より優先される アイドルタイムアウトに合わせる
Domain どのホストへ送るか。指定するとサブドメインにも送られる 指定しない(発行元だけに限定される)
Path どのパス以下へ送るか /
Secure HTTPS のときだけ送る 必須
HttpOnly JavaScript から読めなくする 必須
SameSite クロスサイトのリクエストで送るか Lax(要件次第で Strict

両方あれば Max-Age が勝つ(RFC 6265 で明記)。Expires は絶対時刻なので、クライアントの時計がずれていると効かない。セッションには Max-Age を使う。

どちらも付けなければセッション Cookie(ブラウザを閉じると消える)になる。ただしブラウザのセッション復元機能で生き残ることがあるので、寿命の管理をブラウザに任せない

名前の先頭で属性を強制する仕組み。ブラウザは条件を満たさない Set-Cookie を捨てる。

プレフィックス 強制される条件
__Secure- Secure が付いていること
__Host- Secure が付き、Path=/ であり、Domain が無いこと
__Http- layered-cookies で追加。HttpOnly 相当の制約
__Host-Http- 上の 2 つを合わせたもの

セッション Cookie は __Host- を付ける。 これだけで「HTTPS 限定・サブドメインに漏れない・パスは全体」が名前によって保証される。サーバー側の設定ミスでも属性が落ちない点が価値。

Section titled “1. ブラウザはどの Cookie を送るか”

リクエストごとに、Cookie ストアから条件に合うものを選んで送る。 選定条件はこの順に効く。

flowchart TB
    A["リクエストを送ろうとしている"] --> B{"ホストが Domain と Path に<br/>一致するか"}
    B -->|"しない"| SKIP["送らない"]
    B -->|"する"| C{"Secure 付きなのに<br/>HTTP か"}
    C -->|"HTTP"| SKIP
    C -->|"HTTPS"| D{"有効期限は残っているか"}
    D -->|"切れている"| DEL["ストアから削除して送らない"]
    D -->|"残っている"| E{"クロスサイトの<br/>リクエストか"}
    E -->|"同一サイト"| SEND["Cookie ヘッダに載せる"]
    E -->|"クロスサイト"| F{"SameSite の値は"}
    F -->|"Strict"| SKIP
    F -->|"None かつ Secure"| SEND
    F -->|"Lax または未指定"| G{"安全なメソッドの<br/>トップレベル遷移か"}
    G -->|"はい"| SEND
    G -->|"いいえ"| SKIP

    style SEND fill:#1f5c33,color:#fff
    style SKIP fill:#7a2222,color:#fff

HttpOnly はこの判定に出てこない。 HttpOnly が効くのは「JavaScript から読めるか」だけで、送信するかどうかには影響しない。ここは取り違えやすい。

SameSite=Strict は同一サイトのみ、Lax はトップレベル遷移も通す、None は全部通す

同一サイト クロスサイトのトップレベル遷移(GET) クロスサイトの POST / fetch / img
Strict 送る 送らない 送らない
Lax(未指定時の実質的な既定) 送る 送る(安全なメソッドのみ) 送らない
None 送る 送る 送るSecure 必須)
  • Lax の例外は「安全なメソッドのトップレベル遷移」だけ。 外部サイトのリンクから飛んできたときにログイン状態を保つための穴で、POST は通らない
  • SameSite=NoneSecure が必須。 無いとブラウザは Cookie 自体を捨てる(6265bis に明記
  • Strict にするとログイン状態が切れて見える。 外部リンクから来た初回リクエストで Cookie が送られないため。Lax が既定解になるのはこの実用上の理由
flowchart TB
    subgraph STORE["サーバー側ストア方式"]
        direction TB
        A1["Cookie には ID だけ"]
        A2["Redis や DB に状態を持つ"]
        A3["いつでも失効できる"]
        A4["ストアが単一障害点になる"]
        A1 --> A2 --> A3 --> A4
    end

    subgraph SIGNED["署名付き Cookie 方式"]
        direction TB
        B1["Cookie に状態そのものを入れる"]
        B2["署名か暗号化で改竄を防ぐ"]
        B3["ストアが要らず水平に伸びる"]
        B4["個別の失効ができない"]
        B1 --> B2 --> B3 --> B4
    end

    style A3 fill:#1f5c33,color:#fff
    style A4 fill:#7a4a12,color:#fff
    style B3 fill:#1f5c33,color:#fff
    style B4 fill:#7a2222,color:#fff

選択の軸は「個別に失効させたいか」の一点。 退職・パスワード変更・不審なアクセスへの対応で「今すぐこのセッションを切る」が要るなら、サーバー側ストアにする。署名付き Cookie で失効させるには、結局サーバー側にブラックリストを持つことになり、ステートレスの利点が消える。

消すもの 必須か 消し忘れると
セッションストアのレコード 必須 盗まれた ID がそのまま使える
ブラウザの Cookie(Max-Age=0 望ましい 端末に ID が残る
同じ利用者の他セッション 要件次第 別端末のログインが残る
CSRF トークン セッションに紐付くなら自動 古いトークンが通る

Set-Cookie で消す」だけの実装は、ログアウトになっていない。 ブラウザから消えても、攻撃者の手元にある ID は生きている。

OWASP の Session Management Cheat Sheet が数値で示している。

項目 推奨
エントロピー 最低 64 bit。実務では 128 bit 以上を取る
名前 PHPSESSIDJSESSIONID のような技術スタックが分かる名前を避けるid のような汎用名にする
再発行 認証時・パスワード変更時・権限変更時に必ず作り直す
アイドルタイムアウト 高リスクなら 2〜5 分、低リスクなら 15〜30 分
絶対タイムアウト 4〜8 時間
URL に入れるか 入れない。 URL は Referer・ログ・履歴・共有で漏れる

総当たりの現実性を桁で押さえておく。1 秒に 10 億回試せる攻撃者を仮定して、空間の半分を試すのに必要な時間はこうなる。

エントロピー 必要な時間
32 bit 約 2 秒(実質、破れる)
64 bit 約 292 年
128 bit 約 5.4 × 10^21 年
十分なセッション ID を作る
import secrets
secrets.token_urlsafe(32) # 43 文字 / 256 bit
# => 'ilphHgClQwc-SUQHMdRx0J4X161gc1iVTOzhQyUmpRA'
secrets.token_hex(16) # 32 文字 / 128 bit
# => '7b6c6caaa2816eb4c2ec44fdd0b3c8f5'

random モジュールや Math.random() を使わない。 予測可能な擬似乱数はセッション ID には使えない。暗号論的に安全な生成器(secretscrypto.randomBytescrypto.getRandomValues)を使う。

セッション ID のライフサイクル

Section titled “セッション ID のライフサイクル”
stateDiagram-v2
    [*] --> Anonymous
    Anonymous : 未認証の ID
    Active : 認証済みの ID
    Expired : 期限切れ
    Anonymous --> Active : 認証成功で再発行
    Active --> Active : アクセスで延長
    Active --> Active : 権限変更で再発行
    Active --> Expired : 無操作が続く
    Active --> [*] : ログアウトで破棄
    Expired --> [*] : ストアから削除

Anonymous --> Active の遷移で ID を作り直すのが最重要。同じ ID を使い回すとセッション固定攻撃が通る。

攻撃者が自分の知っている ID を被害者に使わせ、被害者がログインした後にその ID で入り込む。

sequenceDiagram
    autonumber
    participant A as 攻撃者
    participant V as 被害者
    participant S as サーバー

    A->>S: GET / でセッション ID を取得
    S-->>A: Set-Cookie: id=KNOWN
    A->>V: id=KNOWN を仕込むリンクを送る
    V->>S: その ID でログイン
    S->>S: ID を作り直さない実装だと<br/>KNOWN が認証済みになる
    S-->>V: 200 OK
    A->>S: Cookie: id=KNOWN
    S-->>A: 被害者として認証が通る

対策は 1 行。ログイン成功時にセッション ID を再発行する。 フレームワークの API(regenerate / renew / session_regenerate_id)を必ず呼ぶ。加えて、URL やクエリからのセッション ID を受け付けないようにする。

Cookie が自動で送られる性質そのものが原因。 攻撃者のサイトから被害者のブラウザ経由でリクエストを飛ばされると、Cookie が付いてしまう。

flowchart TB
    A["CSRF を防ぎたい"] --> B["SameSite=Lax 以上にする"]
    B --> C{"それだけで足りるか"}
    C -->|"足りない"| D["理由: 古いブラウザ<br/>同一サイト内の別オリジン<br/>SameSite=None が必要な機能"]
    D --> E["CSRF トークンを併用する"]
    E --> F["Origin と Sec-Fetch-Site も検証する"]
    F --> G["状態を変える操作は<br/>GET で受けない"]
    G --> H["多層で防げている"]

    style B fill:#7a4a12,color:#fff
    style H fill:#1f5c33,color:#fff

SameSite は CSRF 対策の一枚目でしかない。 これだけに頼らない理由は 3 つある。

  • 同一サイト判定はスキーム + 登録可能ドメインで行われる。 a.example.com から b.example.com へのリクエストは同一サイト扱いになり、SameSite では止まらない
  • SameSite=None が必要な機能(外部からの決済コールバック、埋め込みウィジェット)を持つと穴が空く
  • Lax は安全なメソッドのトップレベル遷移を通す。 状態を変える操作を GET で受けていると、そこから通る

盗まれた ID をそのまま使われる攻撃。ID を盗ませないのと盗まれても短時間で無効にするの両面で守る。

  • Secure で平文経路に出さない
  • HttpOnly で JavaScript から読めなくする
  • アイドル・絶対タイムアウトを短くする
  • 重要操作の前にパスワードを再確認する

HttpOnly は XSS の対策ではなく、被害の緩和にすぎない。 Cookie が読めなくても、攻撃者のスクリプトは被害者のブラウザ内で任意のリクエストを送れる。Cookie は自動で付くので、ID を盗む必要すらない。

方式 経路に出る値 失効 ステートフルか 主な用途
Basic パスワード(Base64) できない ステートレス 内部ツール、目隠し
Digest ハッシュ できない nonce を持つ 既存機器の互換
Cookie セッション セッション ID サーバー側で破棄できる セッションストアを持つ ブラウザ向けの一般的な Web アプリ
署名付き Cookie 状態そのもの(署名付き) 個別には難しい ステートレス 失効要件が緩い場面
Bearer トークン トークン 有効期限・失効リスト トークンストア次第 API、SPA、モバイル

「ログアウトできる」ことがセッション方式の存在理由。 Basic と Digest はここで落ちる。逆に、ブラウザ以外のクライアント(モバイル、サーバー間)では Cookie の自動送信が邪魔になるので、Bearer に移る。

使ってよいケースと使ってはいけないケース

Section titled “使ってよいケースと使ってはいけないケース”
  • クライアントはブラウザである(Cookie の自動送信が前提の仕組み)
  • HTTPS が強制されているSecure が意味を持つ)
  • セッションストアを運用できる(Redis や DB、あるいは署名付きで失効要件が緩い)
  • 同一サイト内のオリジンを信頼できる(サブドメインを外部に貸していない)
  • CSRF 対策を SameSite 以外にも用意できる
  • セッション ID を暗号論的乱数で生成し、認証時に再発行する実装になっている

Cookie セッションが成立する 6 つの条件

判断の分岐はこの順に辿る。

flowchart TB
    S["Cookie セッションを使いたい"] --> Q1{"クライアントは<br/>ブラウザか"}
    Q1 -->|"モバイル / サーバー間"| NG1["使わない<br/>Bearer トークン"]
    Q1 -->|"ブラウザ"| Q2{"HTTPS が<br/>強制されているか"}
    Q2 -->|"いいえ"| NG2["使わない<br/>まず TLS を用意する"]
    Q2 -->|"はい"| Q3{"個別のセッションを<br/>失効させる必要があるか"}
    Q3 -->|"必要"| Q4{"セッションストアを<br/>運用できるか"}
    Q4 -->|"できない"| NG3["設計を見直す<br/>失効要件かストアのどちらかを変える"]
    Q4 -->|"できる"| OK1["使える<br/>サーバー側ストア方式"]
    Q3 -->|"不要"| OK2["使える<br/>署名付き Cookie も選べる"]

    style OK1 fill:#1f5c33,color:#fff
    style OK2 fill:#1f5c33,color:#fff
    style NG1 fill:#7a2222,color:#fff
    style NG2 fill:#7a2222,color:#fff
    style NG3 fill:#7a4a12,color:#fff
ケース 成立する理由 一緒にやること
ブラウザ向けの一般的な Web アプリ ログアウト・再設定・2 要素認証がすべて作れる __Host- + Secure + HttpOnly + SameSite=Lax + CSRF トークン
管理画面・業務アプリ 権限変更を即座に反映でき、監査もできる SameSite=Strict、短いアイドルタイムアウト、重要操作の再認証
SSR / MPA 構成 サーバーがレンダリングするので Cookie が自然に効く 状態を変える操作を POST に寄せる
同一サイト内の SPA + API Cookie が自動で付くので、トークンを JS で保持しなくてよい CSRF トークンを必須にする。Authorization を使わない設計を明示する

SPA でも Cookie セッションが成立する理由を分解する。 「SPA だから JWT を localStorage に入れる」という選択が広く行われているが、localStorageJavaScript から読めるので XSS で丸ごと盗まれる。同一サイトに API を置けるなら、HttpOnly Cookie のほうが XSS 耐性は高い。トークンを選ぶ理由は「クロスサイトである」「ブラウザ以外もある」であって、「SPA である」ではない。

ケース できないこと
モバイルアプリのネイティブクライアント Cookie ストアの扱いが OS 依存になる。トークンのほうが素直
サーバー間 API Cookie の自動送信という前提がない。ブラウザがいない
クロスサイトで使う埋め込みウィジェット SameSite=None が必要になり、CSRF の穴と 3rd party cookie の廃止に直撃される
第三者アプリへの権限委譲 セッションは「本人がその場にいる」ことしか表せない。OAuth 2.0 の領域
サブドメインを外部に貸している構成 Domain 指定でなくても、同一サイト判定で CSRF を止められない
HTTP しかない経路 Secure が使えず、ID が平文で流れる

Express + Redis(サーバー側ストア)

Section titled “Express + Redis(サーバー側ストア)”
session.js
import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
export const sessionMiddleware = session({
store: new RedisStore({ client }),
name: '__Host-id', // 技術スタックが分かる名前を避ける
secret: process.env.SESSION_SECRET,
resave: false, // 変更が無ければ書き戻さない
saveUninitialized: false, // 未認証で無駄なセッションを作らない
rolling: true, // アクセスごとにアイドル期限を延ばす
proxy: true, // リバースプロキシ配下で Secure を効かせる
cookie: {
path: '/', // __Host- の条件
secure: true, // __Host- の条件
httpOnly: true,
sameSite: 'lax',
maxAge: 30 * 60 * 1000, // アイドルタイムアウト 30 分
// domain は指定しない(__Host- の条件)
},
});
ログイン時の再発行
app.post('/login', async (req, res) => {
const user = await verifyPassword(req.body.id, req.body.password);
if (!user) return res.status(401).render('login', { error: true });
// セッション固定対策。ID を作り直してから認証情報を載せる
req.session.regenerate((err) => {
if (err) return res.sendStatus(500);
req.session.userId = user.id;
req.session.loginAt = Date.now();
req.session.save(() => res.redirect('/mypage'));
});
});
app.post('/logout', (req, res) => {
// サーバー側の破棄が本体
req.session.destroy(() => {
res.clearCookie('__Host-id', { path: '/', secure: true, httpOnly: true });
res.redirect('/');
});
});

saveUninitialized: false は効果が大きい。未認証の訪問者ごとにセッションを作るとストアが無駄に膨らみ、セッション固定の余地も増える。

絶対タイムアウトは自分で見る

Section titled “絶対タイムアウトは自分で見る”

maxAge はアクセスごとに延びる(rolling: true)ので、アイドルタイムアウトにしかならない。絶対タイムアウトは自分で判定する。

絶対タイムアウト
const ABSOLUTE_MS = 8 * 60 * 60 * 1000; // 8 時間
app.use((req, res, next) => {
if (req.session.loginAt && Date.now() - req.session.loginAt > ABSOLUTE_MS) {
return req.session.destroy(() => res.redirect('/login'));
}
next();
});
Set-Cookie を見る
# 属性が意図どおり付いているか確認する
curl -sS -i -X POST https://example.com/login \
-d 'id=demo&password=P@ssw0rd' | grep -i '^set-cookie'
# set-cookie: __Host-id=...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
# Cookie を保存して次のリクエストで送る
curl -sS -c jar.txt -X POST https://example.com/login -d 'id=demo&password=P@ssw0rd'
curl -sS -b jar.txt https://example.com/mypage
# __Host- の条件を満たさない Set-Cookie はブラウザに捨てられる。
# curl は捨てないので、curl で通ってもブラウザで通るとは限らない

RFC 6265 が示すユーザーエージェントが満たすべき下限はこうなっている。

項目 下限
1 Cookie あたり 4096 バイト
1 ドメインあたりの個数 50 個
全体の個数 3000 個

セッション ID だけなら 100 バイト程度で収まる。 署名付き Cookie で状態を詰め込むと 4KB に当たり、しかも毎リクエストで往復するので帯域を食う。Cookie は「小さく保つ」のが原則。

  • Domain を書かない。 書くとサブドメインに広がる
  • セッション Cookie と公開向けサブドメインを混ぜない。 user-content.example.com のような、利用者がコンテンツを置ける場所を同一サイトに持つと、CSRF も XSS も同一サイト扱いで通る
  • 分離したいなら登録可能ドメインを分けるexample-usercontent.com のように別ドメインにする)

クロスサイトの Cookie は主要ブラウザで制限が進んでいる。同一サイトで完結する設計なら影響はない。 影響が出るのは埋め込みウィジェット、クロスドメインの計測、SameSite=None に依存した連携。

Partitioned 属性(CHIPS)はこの用途向けだが、layered-cookies にはまだ入っていない(別ドラフト扱い)。仕様が固まっていない領域だと理解して、依存しない設計を優先する。

Set-Cookie を含む応答を共有キャッシュに入れると、別の利用者にセッション ID が配られる。認証が絡むレスポンスには Cache-Control: private を付ける。CDN の設定で Set-Cookie を落とす・キャッシュキーに含めるといった挙動も確認する。

  • Cookie は認証ではなく運搬手段。 認証状態はサーバー側のセッションが持つ。
  • 発行済み RFC は RFC 6265 だけSameSite とプレフィックスはまだドラフトlayered-cookies が 6265 と 6265bis の両方を廃止する)。
  • Max-AgeExpires より優先される。 Expires はクライアントの時計に依存する。
  • Domain は範囲を広げる属性。 省略すると発行元ホストだけになる。
  • __Host-Secure + Path=/ + Domain 無しを強制する。 セッション Cookie の既定解。
  • SameSite 未指定は仕様上「既定なし」だが、主要ブラウザは Lax 相当として扱う。
  • SameSite=NoneSecure が必須。 無いと Cookie ごと捨てられる。
  • Lax が通すのは「安全なメソッドのトップレベル遷移」だけ。
  • HttpOnly は送信可否に影響しない。 JavaScript から読めるかだけを決める。
  • SameSite だけでは CSRF を防げない。 サブドメイン間は同一サイト扱い。
  • セッション ID はログイン成功時に再発行する。 これがセッション固定対策の本体。
  • ログアウトはサーバー側の破棄が本体。 Cookie を消すだけでは足りない。
  • セッション ID は最低 64 bit、実務では 128 bit 以上。暗号論的乱数で生成する。
  • HttpOnly は XSS 対策ではない。 XSS があれば Cookie を読まずに操作できる。
  • 1 Cookie は 4096 バイト、1 ドメイン 50 個、全体 3000 個が下限。
  • curl は プレフィックス条件や SameSite を強制しない。 ブラウザで確認する。
Cookie とセッションの違いは。

Cookie は「状態をクライアントに置いて毎回送り返させる仕組み」で、運搬手段にすぎない。セッションはサーバー側が持つ認証状態そのもの。Cookie にはその状態を引くための識別子(セッション ID)だけを入れる。

Cookie の仕様はどの文書を見ればよいか。

発行済みなのは RFC 6265(2011)だけだが、これには SameSite も Cookie プレフィックスも載っていない。それらは長らく draft-ietf-httpbis-rfc6265bis にあり、2026 年時点では draft-ietf-httpbis-layered-cookies が RFC 6265 と 6265bis の両方を廃止するとして進んでいる。実務で使っている機能がまだ RFC になっていない領域。

Max-Age と Expires の両方を指定したらどうなるか。

Max-Age が優先される(RFC 6265 に明記)。Expires は絶対時刻なのでクライアントの時計がずれていると効かない。どちらも指定しなければセッション Cookie になり、ブラウザを閉じると消える(ただしセッション復元で生き残ることがある)。

Domain 属性を省略すると Cookie はどこに送られるか。

発行元のホストだけに送られる。Domain=example.com と指定すると *.example.com のサブドメインにも送られるようになる。つまり Domain は範囲を絞る属性ではなく、広げる属性。

`__Host-` プレフィックスは何を強制するか。

Secure が付いていること、Path=/ であること、Domain 属性が無いこと。この 3 つを満たさない Set-Cookie はブラウザが捨てる。「HTTPS 限定・サブドメインに漏れない・パス全体」が名前で保証されるので、セッション Cookie の既定解になる。

SameSite=Lax は何を通し、何を止めるか。

同一サイトのリクエストはすべて通す。クロスサイトでは安全なメソッド(GET など)のトップレベル遷移だけを通し、POSTfetchimg などは止める。外部サイトのリンクから飛んできたときにログイン状態を保つための例外。

SameSite=None を使うときの必須条件は。

Secure を付けること。付いていない SameSite=None の Cookie は、ブラウザがCookie 自体を捨てる(保存しない)。

HttpOnly を付けると Cookie の送信は変わるか。

変わらない。HttpOnly は「JavaScript の API から読めるか」だけを決める。どのリクエストで送られるかは Domain / Path / Secure / SameSite と有効期限で決まる。

SameSite だけで CSRF を防げないのはなぜか。

理由は 3 つ。同一サイト判定はスキームと登録可能ドメインで行われるため a.example.com から b.example.com は同一サイト扱いで止まらない。SameSite=None が必要な機能を持つと穴が空く。Lax は安全なメソッドのトップレベル遷移を通すので、状態を変える操作を GET で受けていると通る。CSRF トークンと Origin / Sec-Fetch-Site の検証を併用する。

セッション固定攻撃とその対策は。

攻撃者が自分の知っているセッション ID を被害者に使わせ、被害者がログインしたあとに同じ ID で入り込む攻撃。対策はログイン成功時にセッション ID を再発行すること。加えて URL やクエリからのセッション ID を受け付けないようにする。

ログアウトの実装で必須なのは何か。

サーバー側のセッションストアからレコードを削除することSet-Cookie でブラウザの Cookie を消すのは後始末で、それだけでは攻撃者の手元にある ID が生きたままになる。

セッション ID に必要なエントロピーはどれくらいか。

OWASP は最低 64 bit を求めている。実務では 128 bit 以上を取る。1 秒に 10 億回試せる攻撃者を仮定すると、32 bit なら約 2 秒で半分を試せてしまうが、64 bit なら約 292 年かかる。生成には暗号論的に安全な乱数(secretscrypto.randomBytes)を使い、randomMath.random() は使わない。

HttpOnly を付けたら XSS は問題にならないか。

ならない。HttpOnly が防ぐのは「ID を持ち出して別の端末から使う」経路だけ。XSS があれば攻撃者のスクリプトは被害者のブラウザ内で任意のリクエストを送れ、Cookie は自動で付くので、ID を読む必要すらない。HttpOnly は緩和であって対策ではない。

サーバー側ストア方式と署名付き Cookie 方式はどう選ぶか。

個別のセッションを即座に失効させたいかの一点で選ぶ。退職・パスワード変更・不審なアクセスへの対応が要るならサーバー側ストア。署名付き Cookie で失効させようとすると、結局サーバー側にブラックリストを持つことになり、ステートレスの利点が消える。

SPA なら JWT を localStorage に置くべきか。

「SPA だから」は理由にならない。localStorage は JavaScript から読めるので、XSS で丸ごと盗まれる。同一サイトに API を置けるなら HttpOnly Cookie のほうが XSS 耐性は高い。トークンを選ぶ理由は「クロスサイトである」「ブラウザ以外のクライアントもある」であって、SPA であること自体ではない。