Cookie とセッション

3 行まとめ
Section titled “3 行まとめ”- Cookie は「状態を運ぶ仕組み」でしかなく、認証そのものではない。 認証状態はサーバー側のセッションが持ち、Cookie はその識別子を往復させるだけ。
- Cookie の安全性は属性で決まる。
Secure/HttpOnly/SameSite/__Host-プレフィックスをどう組むかがそのまま防御力になる。 - Basic や Digest で「できない」とされたこと(ログアウト、パスワード再設定、2 要素認証)が、セッションを持つとすべて可能になる。だから一般利用者向けはこの方式になる。
Cookie とセッションは別のもの
Section titled “Cookie とセッションは別のもの”ここを混ぜると以降がすべてぼやける。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 を混同しない
ログインからログアウトまで
Section titled “ログインからログアウトまで”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 つ。
- ログイン成功時にセッション ID を作り直す。 作り直さないと後述のセッション固定攻撃が成立する。
- ログアウトはサーバー側の破棄が本体。
Set-Cookieで消すだけでは、攻撃者が既に盗んだ ID をそのまま使える。
Set-Cookie の中身を分解する
Section titled “Set-Cookie の中身を分解する”Set-Cookie: __Host-id=ilphHgClQwc-SUQHMdRx0J4X161gc1iVTOzhQyUmpRA; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
| 属性 | 何を決めるか | セッション Cookie での既定解 |
|---|---|---|
Expires |
絶対時刻での失効 | 使わない(Max-Age を使う) |
Max-Age |
秒数での失効。Expires より優先される |
アイドルタイムアウトに合わせる |
Domain |
どのホストへ送るか。指定するとサブドメインにも送られる | 指定しない(発行元だけに限定される) |
Path |
どのパス以下へ送るか | / |
Secure |
HTTPS のときだけ送る | 必須 |
HttpOnly |
JavaScript から読めなくする | 必須 |
SameSite |
クロスサイトのリクエストで送るか | Lax(要件次第で Strict) |
Max-Age と Expires の優先順位
Section titled “Max-Age と Expires の優先順位”両方あれば Max-Age が勝つ(RFC 6265 で明記)。Expires は絶対時刻なので、クライアントの時計がずれていると効かない。セッションには Max-Age を使う。
どちらも付けなければセッション Cookie(ブラウザを閉じると消える)になる。ただしブラウザのセッション復元機能で生き残ることがあるので、寿命の管理をブラウザに任せない。
Cookie プレフィックス
Section titled “Cookie プレフィックス”名前の先頭で属性を強制する仕組み。ブラウザは条件を満たさない Set-Cookie を捨てる。
| プレフィックス | 強制される条件 |
|---|---|
__Secure- |
Secure が付いていること |
__Host- |
Secure が付き、Path=/ であり、Domain が無いこと |
__Http- |
layered-cookies で追加。HttpOnly 相当の制約 |
__Host-Http- |
上の 2 つを合わせたもの |
セッション Cookie は __Host- を付ける。 これだけで「HTTPS 限定・サブドメインに漏れない・パスは全体」が名前によって保証される。サーバー側の設定ミスでも属性が落ちない点が価値。
データフローを追う
Section titled “データフローを追う”1. ブラウザはどの Cookie を送るか
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 から読めるか」だけで、送信するかどうかには影響しない。ここは取り違えやすい。
2. SameSite の 3 つのモード
Section titled “2. SameSite の 3 つのモード”
| 値 | 同一サイト | クロスサイトのトップレベル遷移(GET) | クロスサイトの POST / fetch / img |
|---|---|---|---|
Strict |
送る | 送らない | 送らない |
Lax(未指定時の実質的な既定) |
送る | 送る(安全なメソッドのみ) | 送らない |
None |
送る | 送る | 送る(Secure 必須) |
Laxの例外は「安全なメソッドのトップレベル遷移」だけ。 外部サイトのリンクから飛んできたときにログイン状態を保つための穴で、POSTは通らないSameSite=NoneはSecureが必須。 無いとブラウザは Cookie 自体を捨てる(6265bis に明記)Strictにするとログイン状態が切れて見える。 外部リンクから来た初回リクエストで Cookie が送られないため。Laxが既定解になるのはこの実用上の理由
3. セッションはどこに置くか
Section titled “3. セッションはどこに置くか”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 で失効させるには、結局サーバー側にブラックリストを持つことになり、ステートレスの利点が消える。
4. ログアウトで何を消すか
Section titled “4. ログアウトで何を消すか”| 消すもの | 必須か | 消し忘れると |
|---|---|---|
| セッションストアのレコード | 必須 | 盗まれた ID がそのまま使える |
ブラウザの Cookie(Max-Age=0) |
望ましい | 端末に ID が残る |
| 同じ利用者の他セッション | 要件次第 | 別端末のログインが残る |
| CSRF トークン | セッションに紐付くなら自動 | 古いトークンが通る |
「Set-Cookie で消す」だけの実装は、ログアウトになっていない。 ブラウザから消えても、攻撃者の手元にある ID は生きている。
セッション ID の要件
Section titled “セッション ID の要件”OWASP の Session Management Cheat Sheet が数値で示している。
| 項目 | 推奨 |
|---|---|
| エントロピー | 最低 64 bit。実務では 128 bit 以上を取る |
| 名前 | PHPSESSID や JSESSIONID のような技術スタックが分かる名前を避ける。id のような汎用名にする |
| 再発行 | 認証時・パスワード変更時・権限変更時に必ず作り直す |
| アイドルタイムアウト | 高リスクなら 2〜5 分、低リスクなら 15〜30 分 |
| 絶対タイムアウト | 4〜8 時間 |
| URL に入れるか | 入れない。 URL は Referer・ログ・履歴・共有で漏れる |
総当たりの現実性を桁で押さえておく。1 秒に 10 億回試せる攻撃者を仮定して、空間の半分を試すのに必要な時間はこうなる。
| エントロピー | 必要な時間 |
|---|---|
| 32 bit | 約 2 秒(実質、破れる) |
| 64 bit | 約 292 年 |
| 128 bit | 約 5.4 × 10^21 年 |
import secrets
secrets.token_urlsafe(32) # 43 文字 / 256 bit# => 'ilphHgClQwc-SUQHMdRx0J4X161gc1iVTOzhQyUmpRA'
secrets.token_hex(16) # 32 文字 / 128 bit# => '7b6c6caaa2816eb4c2ec44fdd0b3c8f5'random モジュールや Math.random() を使わない。 予測可能な擬似乱数はセッション ID には使えない。暗号論的に安全な生成器(secrets、crypto.randomBytes、crypto.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 を使い回すとセッション固定攻撃が通る。
セッション固定 (Session Fixation)
Section titled “セッション固定 (Session Fixation)”攻撃者が自分の知っている 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 を受け付けないようにする。
CSRF (Cross-Site Request Forgery)
Section titled “CSRF (Cross-Site Request Forgery)”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で受けていると、そこから通る
セッションハイジャック
Section titled “セッションハイジャック”盗まれた ID をそのまま使われる攻撃。ID を盗ませないのと盗まれても短時間で無効にするの両面で守る。
Secureで平文経路に出さないHttpOnlyで JavaScript から読めなくする- アイドル・絶対タイムアウトを短くする
- 重要操作の前にパスワードを再確認する
XSS と HttpOnly の関係
Section titled “XSS と HttpOnly の関係”HttpOnly は XSS の対策ではなく、被害の緩和にすぎない。 Cookie が読めなくても、攻撃者のスクリプトは被害者のブラウザ内で任意のリクエストを送れる。Cookie は自動で付くので、ID を盗む必要すらない。
他の認証方式との比較
Section titled “他の認証方式との比較”| 方式 | 経路に出る値 | 失効 | ステートフルか | 主な用途 |
|---|---|---|---|---|
| Basic | パスワード(Base64) | できない | ステートレス | 内部ツール、目隠し |
| Digest | ハッシュ | できない | nonce を持つ | 既存機器の互換 |
| Cookie セッション | セッション ID | サーバー側で破棄できる | セッションストアを持つ | ブラウザ向けの一般的な Web アプリ |
| 署名付き Cookie | 状態そのもの(署名付き) | 個別には難しい | ステートレス | 失効要件が緩い場面 |
| Bearer トークン | トークン | 有効期限・失効リスト | トークンストア次第 | API、SPA、モバイル |
「ログアウトできる」ことがセッション方式の存在理由。 Basic と Digest はここで落ちる。逆に、ブラウザ以外のクライアント(モバイル、サーバー間)では Cookie の自動送信が邪魔になるので、Bearer に移る。
使ってよいケースと使ってはいけないケース
Section titled “使ってよいケースと使ってはいけないケース”成立の前提条件
Section titled “成立の前提条件”- クライアントはブラウザである(Cookie の自動送信が前提の仕組み)
- HTTPS が強制されている(
Secureが意味を持つ) - セッションストアを運用できる(Redis や DB、あるいは署名付きで失効要件が緩い)
- 同一サイト内のオリジンを信頼できる(サブドメインを外部に貸していない)
- CSRF 対策を
SameSite以外にも用意できる - セッション ID を暗号論的乱数で生成し、認証時に再発行する実装になっている

判断の分岐はこの順に辿る。
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
使ってよいケース
Section titled “使ってよいケース”| ケース | 成立する理由 | 一緒にやること |
|---|---|---|
| ブラウザ向けの一般的な 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 に入れる」という選択が広く行われているが、localStorage は JavaScript から読めるので XSS で丸ごと盗まれる。同一サイトに API を置けるなら、HttpOnly Cookie のほうが XSS 耐性は高い。トークンを選ぶ理由は「クロスサイトである」「ブラウザ以外もある」であって、「SPA である」ではない。
使ってはいけないケース
Section titled “使ってはいけないケース”| ケース | できないこと |
|---|---|
| モバイルアプリのネイティブクライアント | Cookie ストアの扱いが OS 依存になる。トークンのほうが素直 |
| サーバー間 API | Cookie の自動送信という前提がない。ブラウザがいない |
| クロスサイトで使う埋め込みウィジェット | SameSite=None が必要になり、CSRF の穴と 3rd party cookie の廃止に直撃される |
| 第三者アプリへの権限委譲 | セッションは「本人がその場にいる」ことしか表せない。OAuth 2.0 の領域 |
| サブドメインを外部に貸している構成 | Domain 指定でなくても、同一サイト判定で CSRF を止められない |
| HTTP しかない経路 | Secure が使えず、ID が平文で流れる |
Express + Redis(サーバー側ストア)
Section titled “Express + Redis(サーバー側ストア)”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();});手元で確かめる
Section titled “手元で確かめる”# 属性が意図どおり付いているか確認する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 で通ってもブラウザで通るとは限らない運用で押さえる点
Section titled “運用で押さえる点”サイズと個数の制限
Section titled “サイズと個数の制限”RFC 6265 が示すユーザーエージェントが満たすべき下限はこうなっている。
| 項目 | 下限 |
|---|---|
| 1 Cookie あたり | 4096 バイト |
| 1 ドメインあたりの個数 | 50 個 |
| 全体の個数 | 3000 個 |
セッション ID だけなら 100 バイト程度で収まる。 署名付き Cookie で状態を詰め込むと 4KB に当たり、しかも毎リクエストで往復するので帯域を食う。Cookie は「小さく保つ」のが原則。
ドメイン設計
Section titled “ドメイン設計”Domainを書かない。 書くとサブドメインに広がる- セッション Cookie と公開向けサブドメインを混ぜない。
user-content.example.comのような、利用者がコンテンツを置ける場所を同一サイトに持つと、CSRF も XSS も同一サイト扱いで通る - 分離したいなら登録可能ドメインを分ける(
example-usercontent.comのように別ドメインにする)
3rd party cookie の廃止
Section titled “3rd party cookie の廃止”クロスサイトの Cookie は主要ブラウザで制限が進んでいる。同一サイトで完結する設計なら影響はない。 影響が出るのは埋め込みウィジェット、クロスドメインの計測、SameSite=None に依存した連携。
Partitioned 属性(CHIPS)はこの用途向けだが、layered-cookies にはまだ入っていない(別ドラフト扱い)。仕様が固まっていない領域だと理解して、依存しない設計を優先する。
Set-Cookie を含む応答を共有キャッシュに入れると、別の利用者にセッション ID が配られる。認証が絡むレスポンスには Cache-Control: private を付ける。CDN の設定で Set-Cookie を落とす・キャッシュキーに含めるといった挙動も確認する。
引っかかりやすいポイント
Section titled “引っかかりやすいポイント”- Cookie は認証ではなく運搬手段。 認証状態はサーバー側のセッションが持つ。
- 発行済み RFC は RFC 6265 だけ。
SameSiteとプレフィックスはまだドラフト(layered-cookiesが 6265 と 6265bis の両方を廃止する)。 -
Max-AgeはExpiresより優先される。Expiresはクライアントの時計に依存する。 -
Domainは範囲を広げる属性。 省略すると発行元ホストだけになる。 -
__Host-はSecure+Path=/+Domain無しを強制する。 セッション Cookie の既定解。 -
SameSite未指定は仕様上「既定なし」だが、主要ブラウザはLax相当として扱う。 -
SameSite=NoneはSecureが必須。 無いと 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 など)のトップレベル遷移だけを通し、POST・fetch・img などは止める。外部サイトのリンクから飛んできたときにログイン状態を保つための例外。
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 年かかる。生成には暗号論的に安全な乱数(secrets、crypto.randomBytes)を使い、random や Math.random() は使わない。
HttpOnly を付けたら XSS は問題にならないか。
ならない。HttpOnly が防ぐのは「ID を持ち出して別の端末から使う」経路だけ。XSS があれば攻撃者のスクリプトは被害者のブラウザ内で任意のリクエストを送れ、Cookie は自動で付くので、ID を読む必要すらない。HttpOnly は緩和であって対策ではない。
サーバー側ストア方式と署名付き Cookie 方式はどう選ぶか。
個別のセッションを即座に失効させたいかの一点で選ぶ。退職・パスワード変更・不審なアクセスへの対応が要るならサーバー側ストア。署名付き Cookie で失効させようとすると、結局サーバー側にブラックリストを持つことになり、ステートレスの利点が消える。
SPA なら JWT を localStorage に置くべきか。
「SPA だから」は理由にならない。localStorage は JavaScript から読めるので、XSS で丸ごと盗まれる。同一サイトに API を置けるなら HttpOnly Cookie のほうが XSS 耐性は高い。トークンを選ぶ理由は「クロスサイトである」「ブラウザ以外のクライアントもある」であって、SPA であること自体ではない。