ログアウトは session_destroy() によるセッション破棄、すなわちサーバー側の状態変更操作である。HTTP の仕様上、状態変更操作には POST(または DELETE)を使用する。GET で実装した場合、以下の具体的な問題が発生する。
GET リクエストは <img> タグの src 属性に URL を指定するだけで発火する。
<!-- 攻撃者のサイトに設置されたコード -->
<img src="https://example.com/logout.php" style="display:none" />ブラウザはこの <img> タグを解釈した時点で https://example.com/logout.php に GET リクエストを送信し、PHPSESSID を含む Cookie も自動的に付与する。結果として session_destroy() が実行され、セッションが破棄される。ユーザーの操作は「攻撃者のページを開くこと」のみであり、ログアウトの意思表示は存在しない。
POST の場合、リクエスト送信にはフォームの submit が必要であり、CSRF トークンの検証を追加することで、外部サイトからのリクエストを拒否できる。
RFC 9110 は GET メソッドを「safe」と定義しており、これはリクエストがサーバーの状態を変更しないことを意味する。
| メソッド | RFC 9110 での分類 | 定義 |
|---|---|---|
| GET | safe, idempotent | サーバーの状態を変更しない |
| POST | unsafe | サーバーの状態を変更しうる |
ログアウトは session_destroy() および $_SESSION の破棄(=サーバー側の状態変更)であるため、GET の safe 定義と矛盾する。
ブラウザ・プロキシ・クローラは、GET リンクを safe とみなし、ユーザーの明示的操作なしにリクエストを送信する仕組みを持つ。
- ブラウザのリンクプリフェッチ:
<link rel="prefetch">やブラウザ独自のヒューリスティクスにより、リンク先への GET リクエストが事前に送信される - 検索エンジンのクローラ(Googlebot 等):ページ内の
<a>タグのhrefを辿り、GET リクエストを送信する - ブラウザ拡張機能・セキュリティソフト:ページ内の URL に対して GET リクエストを送信してコンテンツを検査する
これらはいずれも POST リクエストを自動送信しない。POST にすることで、上記の仕組みによるセッション破棄は発生しなくなる。
GET リクエストの URL はブラウザの履歴に記録される。ユーザーが「戻る」ボタンを押下した際、ブラウザは履歴内の GET URL に対して再度リクエストを送信する場合がある。POST リクエストの場合、ブラウザは再送信前に確認ダイアログを表示する仕様(RFC 9110 Section 15.4)であるため、同じ問題は発生しない。
Apache(access.log)や Nginx のデフォルト設定では、GET リクエストの URL(クエリパラメータを含む)がアクセスログに記録される。ログアウト URL に PHPSESSID やトークンがクエリパラメータとして含まれる場合、その値がログファイルに平文で残る。POST の場合、リクエストボディはデフォルト設定ではアクセスログに記録されない。
<?php
// logout.php — GET で実装した場合(非推奨)
session_start();
$_SESSION = [];
session_destroy();
header('Location: /login.php');
exit;この実装では <img src="/logout.php"> や <a href="/logout.php"> 経由でセッションが破棄される。
<?php
// logout.php
session_start();
// POST 以外のメソッドを拒否する
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Allow: POST');
exit;
}
// CSRF トークンを検証する
if (!isset($_POST['_token']) || !hash_equals($_SESSION['_token'], $_POST['_token'])) {
http_response_code(403);
exit;
}
// セッション Cookie を削除する
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// セッションを破棄する
$_SESSION = [];
session_destroy();
header('Location: /login.php');
exit;<!-- ログアウトフォーム(表示側) -->
<form method="POST" action="/logout.php">
<input type="hidden" name="_token" value="<?= htmlspecialchars($_SESSION['_token']) ?>">
<button type="submit">ログアウト</button>
</form>