はじめに
クッキーはクライアント側に保存されるため改竄のリスクがあります。アプリがクッキーの中身をそのまま信頼して認可やログイン判定に使うと、ユーザが自分で値を書き換えて権限を偽装したり、他者のデータにアクセスできてしまいます。ここでは「何が危ないか」「現場で今すぐできる対策」「実装例(防御側)」を端的にまとめます。
1. 概要 — なぜ問題になるか
- クッキーはユーザが自由に編集可能(ブラウザの開発者ツールやスクリプトで変更可)
- 平文で
admin=trueや{"id":1,"admin":false}のような情報を保存し、サーバがそれを信頼していると権限昇格やなりすましが発生する - 攻撃者は改竄で「ログイン済み」「管理者」フラグを作り、保護された機能へアクセスする可能性がある
2. 危険な実装パターン(よくあるアンチパターン)
-
Set-Cookie: admin=trueのように 権限フラグを平文で保存してそのまま判定する - 平文 JSON を
sessioncookie に入れてそのままjson_decode()して権限判定する - 署名や検証をせずに JWT を受け入れてしまう(署名を検証していない、アルゴリズムの落とし穴)
- Cookie に
HttpOnly,Secure,SameSiteを設定していない(XSS/CSRF リスク増大)
3. 安全な基本方針(原則)
- サーバ側でセッション管理を行う(セッションIDのみをクッキーに置き、権限等の実データはサーバ側に保管)。
- クッキーに重要な情報を置かない。どうしても置く場合は 署名(HMAC) か 暗号化 を行い、必ずサーバ側で検証する。
-
Cookie 属性を正しく設定する:
HttpOnly,Secure,SameSite。 - トークンは期限付きかつ一意にし、使い回しを禁止する。
- 既製のセッションライブラリ/フレームワークを使う。手作りでの実装はミスの原因になりやすい。
4. 実装例(防御側・PHP)
A. サーバ管理セッション(推奨)
// セッションはサーバ側(例:PHPのセッション)で権限を管理する
session_start();
// ログイン時(例)
$_SESSION['user_id'] = $userId;
$_SESSION['is_admin'] = $isAdmin; // でも判定は常にサーバ側で行う(DB参照も含める)
// 各リクエストで
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit('Please login');
}
if (!isAdmin($_SESSION['user_id'])) { // DBチェックで確実に検証
http_response_code(403);
exit('Forbidden');
}
- ポイント:クッキーは セッションIDのみ を保持し、権限等はサーバのセッションストア(DB/Redis)で管理する。
B. 署名付きクッキーの簡易例(必要な場合のみ)
// set signed cookie
function set_signed_cookie($name, $value, $secret, $expire=3600) {
$payload = base64_encode(json_encode(['v'=>$value, 'ts'=>time()]));
$sig = hash_hmac('sha256', $payload, $secret);
$cookie = $payload . '.' . $sig;
setcookie($name, $cookie, [
'expires' => time() + $expire,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
}
// verify signed cookie
function verify_signed_cookie($cookie, $secret) {
if (strpos($cookie, '.') === false) return false;
list($payload, $sig) = explode('.', $cookie, 2);
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $sig)) return false;
$data = json_decode(base64_decode($payload), true);
if (!$data) return false;
// optional: check ts for expiry/age
return $data['v'];
}
- 注意点:これも自前実装は落とし穴が多いため、可能なら信頼できるライブラリを使う。
hash_equalsによる定時比較でタイミング攻撃を防ぐ。payload 部分は改竄されても署名で検出できる。
C. JWT を使う場合の注意
- 必ず 署名検証 を行う(公開鍵方式なら RS256 等を利用)
-
algフィールドの改変やnoneアルゴリズム脆弱性に注意 - 有効期限 (
exp) を設定し、リフレッシュ運用を慎重に - JWT に重要な権限フラグを入れる場合、受信時に DB と突合する(可能なら権限は DB 参照)
5. Cookie 属性(必須)
-
HttpOnly:JavaScript から読めないようにする(XSS被害を軽減) -
Secure:HTTPS のみ送信 -
SameSite:CSRF 緩和にLaxかStrictを設定 - 例:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
6. 検出・監視(運用)
-
コードスキャン:リポジトリ内で
Set-Cookieやadmin=true、"admin":など平文で権限を扱う文字列をgrepで検出git grep -n "admin=true" || true git grep -n "setcookie(" || true -
ログ突合:受信したクッキーの内容(ハッシュや署名の有無)とサーバログを照合し、異常な値が来ていないか監視
-
WAF / IDS ルール:典型的な改竄パターン(
admin=trueのようなリクエストヘッダ・クッキー)を検知してアラート -
セッション異常検知:同一セッションIDを複数IPで利用している、短時間で権限変更要求が頻発するなどはアラート
7. テスト(自動化でやること)
- ユニット:署名付きクッキーの署名検証ロジックを検証(改竄を検出すること)
- 統合:ログイン→セッションIDの振舞い、権限がDB側で変わった場合の挙動(古いクッキーで昇格できない)
- 脆弱性スキャン:サードパーティライブラリやJWT実装の設定チェック
-
CIチェック:
git grepベースで「平文で権限を保存しているコード」を検出してPRをブロック(既に提案済みの方式を流用可)
8. すぐやるべき優先タスク(短期)
- すべての
Set-Cookieを検索し、平文の権限フラグがある箇所を即時修正 - Cookie に
HttpOnly/Secure/SameSiteを付与 - 重要情報(権限・認可情報)は 必ずサーバ側で保持
- セッションライブラリやフレームワークの安全設定を確認(例:PHPの session.cookie_httponly = 1 等)
- CI に grep チェックを入れて継続的に検出する
9. チェックリスト(レビュー用)
- Cookie に重要な権限情報(admin, role 等)を平文で置いていないか?
- セッションID以外で認可判定していないか?(サーバ側データで判定)
- Cookie に
HttpOnly/Secure/SameSiteを設定しているか? - 署名付きクッキーや JWT の署名検証を正しく実装しているか?
- 既製のセッション管理ライブラリを使っているか(手作り実装を避ける)?
- CI に「危険なクッキー実装」を検出するルールがあるか?
まとめ
「クッキーは信頼しない。セッションで管理し、署名/属性で守る」
クッキー改竄はシンプルなミスから生じますが、適切な設計(サーバ管理のセッション、署名、Cookie属性)と自動検出(コードスキャン/CI)でかなり防げます。まずはリポジトリ内の setcookie / admin / 平文JSON を grep して、危険箇所を洗い出しましょう。