コース目次 / 第1章

セッションID と Cookie 属性 — 器を、締める

セッションIDが予測できると他人になりすませること、Cookie属性(HttpOnly/Secure/SameSite)が盗難とCSRFの土台を締めることを、自作ログインで確かめます。

第1章 / 全6章目安 約12分この章のゴール: 強いセッションIDと3つのCookie属性の役割を、手を動かして理解する

セッションは、セッションIDという合言葉で運ばれます。この合言葉を握った人が「あなた」になれる——だから、①当てられないこと、②盗まれにくいことが命です。この章で、両方を手を動かして確かめます。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

標的 — セッションIDを切り替えられるログイン

下を session-demo.js として保存します。環境変数 WEAK=1 を付けると予測できるセッションID(連番)、付けなければ強い乱数になります。

// session-demo.js — セッションIDの強弱を切り替えられるログイン。127.0.0.1:3800。
const http = require('node:http');
const crypto = require('node:crypto');

let counter = 0;
const sessions = new Map(); // sid -> user
const WEAK = process.env.WEAK === '1';

function newSid() {
  // WEAK: 連番(1,2,3…)= 予測できる。強い: 16バイトの乱数を16進で。
  return WEAK ? String(++counter) : crypto.randomBytes(16).toString('hex');
}

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:3800');
  const sid = /(?:^|;\s*)sid=([^;]+)/.exec(req.headers.cookie || '')?.[1];

  if (url.pathname === '/login') {
    const s = newSid();
    sessions.set(s, 'haruto');
    // 属性: HttpOnly(JSから隠す)・SameSite=Lax(他サイト起点の送信を絞る)。
    // 本番では Secure(HTTPS限定)も付ける(localhost は http なのでここでは省略)。
    res.setHeader('Set-Cookie', 'sid=' + s + '; HttpOnly; SameSite=Lax; Path=/');
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('ログインしました。sid=' + s);
    return;
  }
  if (url.pathname === '/me') {
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end(sid && sessions.has(sid) ? 'あなたは ' + sessions.get(sid) + ' さん' : '未ログイン');
    return;
  }
  res.end('ok');
}).listen(3800, '127.0.0.1', () => console.log('セッションデモ: http://127.0.0.1:3800 (Ctrl+C で停止)'));

弱いセッションID — 当ててみる

まず弱い方で起動します。

WEAK=1 node session-demo.js

/login を2回叩いてみます(別々の“利用者”のつもりで)。

curl -i http://127.0.0.1:3800/login    # sid=1
curl -i http://127.0.0.1:3800/login    # sid=2

セッションIDが 1、2、3…と連番で発行されています。あなたが sid=5 をもらったなら、他の利用者は 1〜4 や 6 を持っているだろう、とすぐ推測できる。実際、他人の sid を当てて送れば、その人として通ってしまいます。

# 攻撃者が「他人の sid」を推測して送る
curl -H "Cookie: sid=1" http://127.0.0.1:3800/me    # => あなたは haruto さん

セッションIDを知らないユーザーの sid を、当てるだけでなりすませた。 これがセッション予測です。連番、時刻、ユーザー名由来——推測できるセッションIDは、それだけで穴です。

強いセッションID — 当てられない

Ctrl+C で止め、今度は WEAK=1 を外して起動します。

node session-demo.js
curl -i http://127.0.0.1:3800/login
# sid=9f2c1a...(32文字の16進、毎回まったく違う)

セッションIDが crypto.randomBytes(16) 由来の予測不能な乱数になりました。2^128 通りの空間から選ばれるので、総当たりも推測も現実的に不可能。セッションIDは、暗号的な乱数で十分に長く——これが第一の守りです(自前で作らず、実績あるセッション管理ライブラリに任せるのが基本ですが、その中身はこれです)。

当てられなくても、盗まれたら同じこと。盗難を難しくするのが、Set-Cookie の属性です。デモの Set-Cookie に付けた属性を読みます。

HttpOnly — この Cookie を、JavaScript から読めなくする(document.cookie に出てこない)。万一 XSS が起きても、セッションIDを直接は盗み出せない。sec-jwt の HttpOnly と同じ。
Secure — この Cookie を、HTTPS の通信でだけ送る。http の平文に載って盗聴されるのを防ぐ。本番では必須(localhost は http なのでデモでは省略)。
SameSite — この Cookie を、他サイト起点のリクエストでは送らない/絞る。Strict(他サイト起点では一切送らない)、Lax(トップレベル遷移など一部だけ送る/既定)、None(常に送る・要 Secure)。これが次章以降のCSRFへの第一防御になります。

curl -i http://127.0.0.1:3800/login のレスポンスヘッダで、Set-Cookie: sid=…; HttpOnly; SameSite=Lax; Path=/ が乗っているのを確かめてください。診断では、この属性の欠落を必ず見ます——HttpOnly が無ければ XSS でセッションを盗まれ、SameSite が無ければ CSRF が通りやすい。

持ち帰る一言

当てられず、盗まれにくく。 セッションIDは暗号乱数で長く、Cookie には HttpOnly・Secure・SameSite を付ける。連番のセッションID、属性の欠落——診断で最初に見る基本です。次は、攻撃者が「自分の知っているセッション」を掴ませる手口——セッション固定です。

こうなっていればOK

卒業まであと4章です。

この章はまだ完了していません。