セッション管理の原理(セッション固定・ハイジャック対策)
ログイン状態を支えるセッションIDが盗まれれば、パスワードなしで成りすまされる。ID生成のエントロピー要件と Cookie 属性の正しい設定を原理から押さえ、固定化・ハイジャックを確実に塞げる。
- 1.セッションはサーバ側保持(IDだけ渡す)かステートレス(署名付きトークンに状態を入れる)の二択。前者は失効が即時、後者はスケールしやすいが取り消しが難しいトレードオフがある。
- 2.セッションIDは推測不能が絶対条件。CSPRNG 由来で最低128ビットのエントロピーを持たせ、ログイン直後にIDを再発行(regenerate)してセッション固定攻撃を無効化する。
- 3.ハイジャック・サイドジャッキングは Cookie を盗ませない設計で防ぐ。HttpOnly で JS から隔離、Secure で平文送信を禁止、SameSite で別サイト送信を絞るのが基本三点セット。
なぜセッションが必要なのか
HTTP は本来ステートレスで、各リクエストは互いに独立しています。サーバーは「いま届いたこのリクエストが、さっきログインした人と同じ人か」を、リクエスト単体からは判断できません。そこで一度だけ本人確認(認証)を済ませ、その結果を表す秘密の合言葉を発行し、以降のリクエストに付けてもらう——これがセッションの本質です。
つまりセッションIDは「ログイン状態そのもの」を代理する値です。だからこの値が漏れれば、攻撃者はパスワードを知らなくても本人として振る舞えます。セッション管理の設計とは、突き詰めれば「この合言葉をどう作り、どう運び、どう守るか」に尽きます。
サーバ側セッション vs ステートレス
セッション状態をどこに置くかで、二つの方式に大別されます。
| 観点 | サーバ側セッション | ステートレス(署名付きトークン) |
|---|---|---|
| 保存場所 | サーバー(メモリ/Redis/DB)。Cookie にはIDのみ | クライアント。状態を本体に詰めて持たせる |
| 検証方法 | IDでストアを引き、状態を取り出す | サーバーの鍵で署名(HMAC等)を検証する |
| 即時失効 | ストアから消せば即無効。容易 | 有効期限まで原則有効。取り消しが難しい |
| スケール | 共有ストアが要る(水平展開の足かせ) | サーバーが状態を持たず展開しやすい |
| 改ざん耐性 | 状態はサーバー内なので安全 | 署名で保証。鍵漏洩・alg混乱に注意 |
ステートレス方式(JWT がその代表)は「状態をクライアントに持たせ、署名で改ざんを検出する」発想です。スケールしやすい反面、サーバーは発行済みトークンを覚えていないため、「ログアウト即無効化」や「漏洩したトークンの強制失効」が苦手です。これを補うには有効期限を短くし、失効リスト(ブロックリスト)を別途持つ——結局サーバー側に状態が戻ってくる、というジレンマがあります。
署名付きトークンは「鍵で検証が通る限り有効」です。盗まれたトークンは、有効期限が切れるまでサーバー側からは止められません。重要操作を扱うシステムでは、短い有効期限+リフレッシュ、あるいはサーバ側セッションへの回帰を検討します。「JWT を長命セッションIDとして Cookie に入れる」のは両者の悪いとこ取りになりがちです。
セッションID生成のエントロピー要件
セッションIDの安全性は、ただ一点「攻撃者が当てられないこと」に懸かっています。逐次的なID(user1001, user1002...)や時刻ベースの値は、推測・列挙されて即座に破られます。要件は明確です。
- 予測不可能性:暗号論的に安全な乱数(CSPRNG)で生成する。
Math.random()やrand()などの一般用途の擬似乱数は内部状態を逆算され得るため厳禁。 - 十分なエントロピー:OWASP は最低 64 ビット、実務では 128 ビット以上を推奨。これは総当たりや、有効IDが空間にどれだけ密に存在するか(在席率)を踏まえた基準です。
- 当て推量への耐性:攻撃者が秒間に試せる回数を踏まえ、有効なIDを引き当てる確率を無視できる水準に下げる。
ざっくり言うと、安全性の余裕は「IDのビット長」と「同時に有効なセッション数」の差で決まります。128 - log2(有効セッション数) がおおよその実効ビット数で、これが大きいほど推測は非現実的になります。たとえば100万セッション(約 2^20)が有効でも、128ビットIDなら実効 108 ビットが残り、総当たりは事実上不可能です。
# 128ビット = 16バイトを CSPRNG から取り、URLセーフに符号化する例
session_id = base64url( CSPRNG_bytes(16) ) # 例: "kJ3xQ9...22文字程度"
# NG: ユーザーID連番、時刻、UUIDv1(MAC/時刻由来で予測余地あり)
# OK: CSPRNG 由来の十分長いランダム値(UUIDv4 でも可だが長さ・出所に注意)
強いIDは推測を防ぎますが、後述のハイジャックは「推測せずに本物を盗む」攻撃です。エントロピー要件は必要条件であって十分条件ではありません。生成(強いID)と運搬(Cookie 属性)の両輪が揃って初めてセッションが守られます。
三つの攻撃:固定・ハイジャック・サイドジャッキング
代表的な三つの攻撃は、狙う場所が異なります。混同せず、それぞれに対策を当てるのが要点です。
| 攻撃 | やること | 本質的な弱点 | 効く対策 |
|---|---|---|---|
| セッション固定 | 攻撃者が用意したIDを被害者にログインさせる | ログイン後もIDが変わらないこと | ログイン時にIDを再発行(regenerate) |
| セッションハイジャック | 有効なIDを盗み、本人に成りすます | IDが漏れること(XSS・ログ・盗聴) | HttpOnly・Secure・短命化・再認証 |
| サイドジャッキング | 同一網の平文通信からCookieを傍受 | HTTP(非暗号)でCookieが流れること | 全面HTTPS+Secure属性+HSTS |
セッション固定(Session Fixation) は少し直感に反します。攻撃者はまず自分で有効なセッションIDを取得し、それを被害者に押し付けます(URL パラメータや Cookie 注入で)。被害者がそのIDのままログインすると、攻撃者は「自分が知っているID」でログイン済み状態に相乗りできます。急所は「ログイン前後でIDが変わらない」点です。だから対策は単純で、認証成功の瞬間に必ずIDを作り直します。
// ログイン成功時:古いIDを捨てて新しいIDを発行する(固定化を無効化)
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body); // 本人確認
if (!user) return res.status(401).send('Unauthorized');
await req.session.regenerate(); // ← 旧IDを破棄し新IDを採番
req.session.userId = user.id; // 認証情報は再発行後に格納
res.redirect('/dashboard');
});
セッションハイジャック は有効なIDを盗む攻撃の総称です。代表経路は XSS による Cookie 読み取り、URL に載ったIDの Referer・ログ漏れ、共有端末での残存などです。盗まれたIDはそのまま本人の権限で使えてしまいます。
サイドジャッキング(Sidejacking) はハイジャックの一種で、同一ネットワーク(公衆 Wi-Fi など)で平文 HTTP のセッション Cookie を傍受します。Firesheep が有名にした古典的攻撃で、対策は「Cookie を平文で流さない」こと、すなわち全面 HTTPS と Secure 属性に尽きます。
Cookie 属性による防御
セッションIDの運搬は通常 Cookie です。Cookie の属性は、上の攻撃を運搬経路で塞ぐための制御スイッチです。詳細な属性挙動は Web 認証と Cookie でも扱っていますが、セッション保護の観点で要点を押さえます。
| 属性 | 効果 | 防げる攻撃 |
|---|---|---|
| HttpOnly | document.cookie からの読み取りを禁止 | XSS 経由のID窃取(ハイジャック) |
| Secure | HTTPS 接続でのみ送信(平文では送らない) | サイドジャッキング・盗聴 |
| SameSite | 別サイト発リクエストへの自動付与を制限 | CSRF・一部のID持ち出し |
| __Host- 接頭辞 | Secure必須・Path=/・Domain指定不可を強制 | サブドメインからのCookie上書き/固定 |
HttpOnly は JavaScript からセッション Cookie を切り離します。XSS が起きても document.cookie ではIDを読めないため、窃取のハードルが上がります(ただし XSS 下でも「ブラウザに送らせる」操作は可能なので、XSS 自体を出さないことが前提)。
Secure は Cookie を HTTPS 上でしか送らせません。これがないと、たとえ一度でも HTTP でアクセスした瞬間にIDが平文で流れ、サイドジャッキングの餌食になります。さらに Strict-Transport-Security(HSTS)で常時 HTTPS を強制すると、初回の平文アクセス自体を防げます。
SameSite は別サイト発のリクエストにIDを自動付与しない設定で、主に CSRF を緩和します。Lax(多くのブラウザの既定)で書き込み系のクロスサイト送信が止まり、Strict はさらに厳格、None は明示的に別サイト送信を許す代わりに Secure 必須です。
# セッション Cookie の堅牢な設定例(属性を重ねて防御する)
Set-Cookie: __Host-sid=kJ3xQ9...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
# ^__Host- で Domain 注入を封じ ^JSから隔離 ^HTTPS限定 ^CSRF緩和 ^短命化
HttpOnly は「盗ませない」、Secure は「平文で流さない」、SameSite は「他サイトに付けない」と、守る面が違います。どれか一つでは穴が残ります。さらに、これらは Cookie の運搬を守るだけで、サーバー側の寿命管理(アイドルタイムアウト・絶対タイムアウト・ログアウトでの確実な破棄)とは別問題です。両方を揃えて初めてセッションが安全になります。
実務でのまとめ
原理を踏まえると、堅牢なセッション管理は次の三層に整理できます。
- 生成:CSPRNG 由来・128 ビット以上の推測不能なIDを使う。
- 運搬:
HttpOnly; Secure; SameSiteを必ず付け、可能なら__Host-接頭辞と HSTS を併用する。 - 寿命:ログイン時にIDを再発行(固定化対策)、アイドル/絶対タイムアウトを設け、ログアウトでサーバー側状態を確実に破棄する。
加えて、特権操作の前には再認証(MFA を含む)を挟み、盗まれたIDで重要操作まで一気に通らない設計にします。フレームワーク標準のセッション機構(Rails・Django・Spring Security 等)はこれらの多くを既定で備えているので、**「切らずに、正しい属性で使う」**のが最短の堅牢化です。
セキュリティ Article
セッション管理の原理(セッション固定・ハイジャック対策)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セキュリティ
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
セッションIDは推測不能が絶対条件。CSPRNG 由来で最低128ビットのエントロピーを持たせ、ログイン直後にIDを再発行(regenerate)してセッション固定攻撃を無効化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「セキュリティ / セッション」に近いか確認する。
- 強みである「セッションはサーバ側保持(IDだけ渡す)かステートレス(署名付きトークンに状態を入れる)の二択。前者は失効が即時、後者はスケールしやすいが取り消しが難しいトレードオフがある。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。