0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0211(2025/08/13)Webセキュリティのためのセッション管理

Posted at

Webセキュリティのためのセッション管理|実践ガイド

セッション管理とは「ユーザを識別する小さな“鍵”(セッションID)を、安全な経路・安全な保管・安全なライフサイクルで運ぶ設計と運用」である。


前提と用語の最小整理

  • セッションID:サーバ側の状態に紐づく不透明な識別子(推奨)。

  • クッキー:ブラウザが自動送出する保存場所。セッションIDはクッキー一択で保持。

  • 主なクッキー属性

    • HttpOnly(JS不可視) / Secure(HTTPSのみ) / SameSite(CSRF抑制)
    • __Host- プレフィックス(Path=/、Secure、Domain未指定のとき使用可)
  • タイムアウト:アイドル(無操作)と絶対(存続上限)の二軸で設計。

  • HSTS:常時HTTPSを強制するブラウザポリシー。


必須10選(ここだけは絶対に外さない)

1. フレームワークのセッション機構を使い、既定値を監査

目的:車輪の再発明を避け、既知の安全策を享受。
実装:公式ミドルウェアを有効化、既定のSameSite/Secure/HttpOnlyを確認。
落とし穴:リバースプロキシ配下でtrust proxy未設定だとSecureが付かない。

2. 常時HTTPS + HSTS(preload含む)

目的:平文漏洩・ダウングレード阻止。
実装Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

3. セッションクッキーの堅牢化

目的:盗難・スクリプト窃取・CSRFを軽減。
設定最小値HttpOnly; Secure; SameSite=Lax; Path=/。可能なら__Host-sessionid

4. URLにセッションIDを入れない

目的:リファラ・ログ・ブックマークによる漏洩防止。
実装:URLリライティング(SID含め)を全面無効。

5. ログイン成功・権限昇格時のセッションID再発行

目的:セッション固定化攻撃の無効化。
実装regenerate()→旧ID無効→新ID発行。

6. ログアウトはサーバ側で無効化(Cookie削除だけでは不十分)

目的:残存セッションの悪用阻止。
実装:ストアからセッション破棄+Set-Cookie: Max-Age=0

7. タイムアウト設計(アイドル+絶対)

目的:放置端末・長期乗っ取りのリスク低減。
目安:アイドル15–30分、絶対8–24時間。重要操作は再認証。

8. CSRF対策を“Cookie運用”と一体化

目的:クロスサイト起点の偽操作防止。
実装:同期トークン(フォーム/ヘッダ)+SameSite。SPAでもCookie認証ならCSRF必須

9. XSS対策を前提に置く

目的:XSSはセッションを間接的に破る。
実装:テンプレート自動エスケープ、入力/出力の文脈エスケープ、CSP(script-src 'self'など)。

10. 共有セッションストアとTTL

目的:スケールと安定(スティッキー回避)。
実装:Redis等に保存、アクセス時にTTL延長。権限昇格時は別ネームスペース。


応用10選(リスクとUXを両立させる)

1. 「Remember me」は別トークンで設計

  • 長寿命トークンは単回使用+ローテーション、DBはハッシュ保存

2. ステップアップ認証

  • 送金・設定変更などの高リスク操作でパスワード再入力やMFA。

3. リスクベースの異常検知

  • UA/IP/ASN/地理急変を検出し再認証セッション失効

4. 同時セッション管理と全端末ログアウト

  • ユーザUIで「他端末からサインアウト」を提供。パスワード変更時は全失効。

5. OAuth/OIDC連携の落とし穴

  • state/nonce検証、PKCE必須。コールバック直後にセッションID再発行

6. 管理画面と一般サイトのクッキー分離

  • 管理は別サブドメインにし、作用域を最小化。

7. JWT vs サーバセッションの使い分け

  • 短寿命JWTはAPIに有効だが失効が難しい。一般Webは不透明IDが扱いやすい。

8. 監査ログとアラート

  • ログイン/失敗/再発行/失効を記録。短時間での地点変動は通知。

9. 逆プロキシ配下の信頼ヘッダ

  • X-Forwarded-Proto等を正しく信頼設定。誤るとSecureが外れうる。

10. キャッシュ制御

  • 機微ページはCache-Control: no-store、履歴に残さない配慮。

比較表(同じ論点は表で押さえる)

クッキー vs WebStorage(ブラウザでの保管場所)

保管場所 自動送信 JSから参照 CSRF影響 XSS影響 用途の推奨
Cookie(HttpOnly) あり なし 受ける 低減 セッションID
Cookie(非HttpOnly) あり あり 受ける 受ける 原則非推奨
localStorage/sessionStorage なし あり 受けにくい 受ける 非機微キャッシュのみ

SameSiteの違い

サードパーティ送出 主な使い所
Strict しない ほぼ純粋な同一サイトのみ
Lax ナビゲーション等は可 通常のWebに推奨デフォルト
None+Secure する 外部IdP/クロスサイト必須時のみ

JWT vs サーバセッション

観点 JWT(自己完結) サーバセッション(不透明ID)
失効の容易さ 容易(サーバ破棄)
ステートレス ×(ストア必要)
漏洩時の影響 トークン寿命まで有効 サーバ側で即失効可
Web向き 条件付き

図解:セッションのライフサイクル(概念図)

[Browser] -- HTTPS/login ---> [Server]
             (認証OK)
[Server] -- Set-Cookie: __Host-sessionid; HttpOnly; Secure; SameSite=Lax --> [Browser]
[Browser] -- Cookie送出 --> [Server] (通常操作)
(権限昇格) -> [Server] session regenerate -> 新ID発行 -> Set-Cookie
(ログアウト) -> [Server] セッション破棄 -> Set-Cookie: Max-Age=0

図解:信頼境界と保存先

+------------------+          +-----------------------+
|  Browser         |  HTTPS   |   Application Server  |
|  - Cookie Store  | <------> |   - Session Store     |
|                  |          |     (Redis/DB)        |
+------------------+          +-----------------------+
      ^   ^                            |
      |   |                            | TTL/失効/再発行
   JS参照不可(HttpOnly)         監査ログ/検知

最小チェックリスト(コピペ運用用)

  • HSTS有効化(includeSubDomains; preload
  • __Host-sessionid; HttpOnly; Secure; SameSite=Lax(必要時のみNone
  • ログイン/権限昇格時にregenerate()
  • ログアウトでサーバ側無効化+Cookie失効
  • アイドルX分・絶対Y時間のタイムアウト
  • Cookie作用域最小化(管理系は分離)
  • CSRF:同期トークン + SameSite
  • XSS:自動エスケープ + CSP
  • 共有ストアTTL・アクセス延長
  • 監査ログ・アラート(失敗/再発行/失効)

失敗しやすいポイント(あるある)

  • 逆プロキシでhttpsが伝わらずSecureが付かない。
  • 外部IdP導入でSameSite=NoneにしたのにSecureを付け忘れる。
  • ログアウトをCookie削除だけで済ませ、サーバ側が生きている。
  • JWTを長寿命にしてブラックリストなしで運用。

参考実装断片(イメージ)

Express (Node.js)

app.set('trust proxy', 1);
app.use(session({
  name: '__Host-sessionid',
  secret: process.env.SESSION_SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', path: '/' },
  resave: false, saveUninitialized: false,
  rolling: true,
  store: new RedisStore({ /* ttl等 */ })
}));
// 認証成功時
req.session.regenerate(err => { /* 旧ID失効→新ID */ });

Django (settings.py)

SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"  # 外部IdP時は "None"
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

まとめ

  • 必須10選で“落とし穴を塞ぐ”のが先、応用10選で“攻めの運用”を足す。
  • 迷ったら:不透明ID + Cookie + 短寿命 + サーバ側失効 + 再発行が基本形。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?