中小企業の業務システムで頻発するPHPの認証バグ。セッションとCookieの混同が原因のケースが多い。正しい使い分けと実装パターンを解説します。
こんな悩み、ありませんか?
「ログインしたはずなのに、しばらくするとセッションが切れる」「別のユーザーでログインしたら、なぜか前のユーザーの情報が表示された」「本番環境に上げたらログインが通らない……」
業務システムの保守を担当するWeb担当者から、こういった相談が定期的に届きます。これらのトラブルの多くは、PHPのセッションとCookieの違いを曖昧に理解したまま実装してしまったことが根本原因です。
特に中小企業の業務システムは、昔に外注して作ったコードをそのまま使い続けていたり、複数の担当者がパッチを当て続けたりしていることが多い。気がつけば「誰も全体を把握していない認証まわり」になっているケースは珍しくありません。
この記事では、セッションとCookieの本質的な違いから、実際に使うべき場面の判断基準、そしてよくある実装ミスのパターンまでを、コード例を交えながら整理します。
セッションとCookieは「何が違うのか」を正確に理解する
まず前提として、両者の役割の違いを押さえておきましょう。
| 項目 | セッション | Cookie |
|---|---|---|
| データの保存場所 | サーバー側 | クライアント(ブラウザ)側 |
| 有効期限のデフォルト | ブラウザを閉じるまで | 設定次第で長期保持が可能 |
| 容量の上限 | サーバーリソースに依存 | 約4KB |
| セキュリティリスク | 相対的に低い | 改ざん・盗聴のリスクあり |
| 主な用途 | ログイン状態・一時データ | ユーザー設定・自動ログイン |
セッションはサーバー側にデータを持ち、ブラウザにはそのIDだけを渡します。Cookieはデータそのものをブラウザに保存します。この違いが、セキュリティ要件の高い認証処理において致命的な差を生むことがあります。
認証情報(ユーザーIDや権限情報)は必ずセッションに保存するというのが基本原則です。Cookieに直接保存することは、データの改ざんリスクを生む行為です。
よくある認証バグの原因:実案件から見えたパターン
神奈川の製造業クライアントから相談を受けた案件での実例をご紹介します。社内向けの受注管理システムで、特定のユーザーがログインすると別のユーザーのデータが見えてしまうという深刻なバグが発生していました。
調査してみると、問題のコードはこのようなものでした。
// ❌ 問題のある実装例
setcookie('user_id', $userId, time() + 3600);
setcookie('role', $userRole, time() + 3600);
// 認証チェック
$userId = $_COOKIE['user_id'];
$role = $_COOKIE['role'];
CookieにユーザーIDと権限を直接書き込んでいたため、ブラウザの開発者ツールから値を書き換えるだけで、別ユーザーとして振る舞える状態になっていたのです。さらに、CookieのHttpOnly属性が設定されておらず、JavaScriptからもアクセスできる状態でした。
これは単なる実装ミスではなく、設計段階での認識不足が原因です。なぜその方法を選んだか確認したところ、「セッションだとサーバーに負荷がかかると聞いた」という理由でした。小規模な業務システムであれば、セッションの負荷を心配する必要はほぼありません。
正しい実装パターン:セッションと署名付きCookieの使い分け
基本:ログイン認証はセッションで管理する
Laravelを使っている場合は標準のセッション管理に任せるのが最善ですが、素のPHPで書かれたシステムの場合は以下のパターンを参考にしてください。
<?php
// セッションの開始(必ずページ冒頭で)
session_start();
// ログイン処理
function loginUser(int $userId, string $role): void {
// セッションIDを再生成してセッション固定攻撃を防ぐ
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['role'] = $role;
$_SESSION['login_at'] = time();
}
// 認証チェック
function isAuthenticated(): bool {
if (empty($_SESSION['user_id'])) {
return false;
}
// セッションタイムアウトチェック(30分)
if (time() - $_SESSION['login_at'] > 1800) {
session_destroy();
return false;
}
// タイムスタンプを更新
$_SESSION['login_at'] = time();
return true;
}
応用:「次回から自動ログイン」はCookieを正しく使う
「ログイン状態を30日間保持する」のような機能は、Cookieを使う正当なユースケースです。ただし、CookieにユーザーIDを直接保存するのはNG。推測不可能なトークンを保存し、サーバー側でDBと突合する設計にします。
<?php
// ✅ 自動ログイン用トークンの発行
function issueRememberToken(int $userId, PDO $pdo): void {
// 暗号学的に安全なランダムトークン生成
$token = bin2hex(random_bytes(32));
$hashed = hash('sha256', $token);
$expiry = date('Y-m-d H:i:s', time() + 86400 * 30);
// DBにハッシュ化したトークンを保存
$stmt = $pdo->prepare(
'INSERT INTO remember_tokens (user_id, token_hash, expires_at)
VALUES (?, ?, ?)'
);
$stmt->execute([$userId, $hashed, $expiry]);
// Cookieにはトークンのみをセット
setcookie(
'remember_token',
$token,
[
'expires' => time() + 86400 * 30,
'path' => '/',
'secure' => true, // HTTPS必須
'httponly' => true, // JavaScript から読めないように
'samesite' => 'Lax', // CSRF対策
]
);
}
ポイントは3つです。①トークン自体はDBにハッシュ化して保存、②CookieにはHTTPS必須のsecure属性を付与、③HttpOnly属性でJavaScriptからのアクセスを遮断。この3点を外すと、脆弱なまま動作してしまいます。
よくある失敗パターンと対処法
セキュリティ設定のステータスを可視化する
実際の案件でセキュリティ診断を行うと、以下のような改善余地が見つかることが多いです。
上のレーダーチャートはあるクライアントの実際の診断イメージをもとに作成したものです。「動いているから問題ない」と思っているシステムが、セキュリティ面では複数の穴を抱えているケースは非常に多い。
開発・運用でお困りなら
システム開発
設計から運用まで、堅牢なシステムを構築します
※ 通常1営業日以内にご返信します
まとめと次のステップ
PHPのセッションとCookieは、役割が似ているように見えて根本的に異なる仕組みです。「認証情報はサーバー側のセッションで管理する」「Cookieを使う場合は属性設定を徹底する」この2点を守るだけで、業務システムの認証まわりの脆弱性の大半は防げます。
既存システムのコードが今どういう状態になっているか、一度確認してみてください。特に「何年も触っていない認証まわりのコード」は、今の水準から見ると危険な実装になっていることが少なくありません。
自社システムの現状が気になった方は、Fivenine Designへコードレビューや改修のご相談をお気軽にどうぞ。神奈川を拠点に20年以上、中小企業の業務システム開発に携わってきた実績から、実用的な改善提案をお伝えできます。