Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Last active February 21, 2026 07:06
Show Gist options
  • Select an option

  • Save kobitoDevelopment/5d0283cdb86ca455a3c9d5fbf694302d to your computer and use it in GitHub Desktop.

Select an option

Save kobitoDevelopment/5d0283cdb86ca455a3c9d5fbf694302d to your computer and use it in GitHub Desktop.

ログアウト処理を GET で構築してはいけない理由(PHP)

結論

ログアウトは session_destroy() によるセッション破棄、すなわちサーバー側の状態変更操作である。HTTP の仕様上、状態変更操作には POST(または DELETE)を使用する。GET で実装した場合、以下の具体的な問題が発生する。


1. CSRF(クロスサイトリクエストフォージェリ)攻撃が成立する

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 トークンの検証を追加することで、外部サイトからのリクエストを拒否できる。

2. HTTP の仕様(RFC 9110 Section 9.2.1)に違反する

RFC 9110 は GET メソッドを「safe」と定義しており、これはリクエストがサーバーの状態を変更しないことを意味する。

メソッド RFC 9110 での分類 定義
GET safe, idempotent サーバーの状態を変更しない
POST unsafe サーバーの状態を変更しうる

ログアウトは session_destroy() および $_SESSION の破棄(=サーバー側の状態変更)であるため、GET の safe 定義と矛盾する。

3. プリフェッチ・クローラがリクエストを送信する

ブラウザ・プロキシ・クローラは、GET リンクを safe とみなし、ユーザーの明示的操作なしにリクエストを送信する仕組みを持つ。

  • ブラウザのリンクプリフェッチ<link rel="prefetch"> やブラウザ独自のヒューリスティクスにより、リンク先への GET リクエストが事前に送信される
  • 検索エンジンのクローラ(Googlebot 等):ページ内の <a> タグの href を辿り、GET リクエストを送信する
  • ブラウザ拡張機能・セキュリティソフト:ページ内の URL に対して GET リクエストを送信してコンテンツを検査する

これらはいずれも POST リクエストを自動送信しない。POST にすることで、上記の仕組みによるセッション破棄は発生しなくなる。

4. ブラウザ履歴・キャッシュに URL が残る

GET リクエストの URL はブラウザの履歴に記録される。ユーザーが「戻る」ボタンを押下した際、ブラウザは履歴内の GET URL に対して再度リクエストを送信する場合がある。POST リクエストの場合、ブラウザは再送信前に確認ダイアログを表示する仕様(RFC 9110 Section 15.4)であるため、同じ問題は発生しない。

5. アクセスログに URL 全体が記録される

Apache(access.log)や Nginx のデフォルト設定では、GET リクエストの URL(クエリパラメータを含む)がアクセスログに記録される。ログアウト URL に PHPSESSID やトークンがクエリパラメータとして含まれる場合、その値がログファイルに平文で残る。POST の場合、リクエストボディはデフォルト設定ではアクセスログに記録されない。


誤った実装(GET)

<?php
// logout.php — GET で実装した場合(非推奨)
session_start();
$_SESSION = [];
session_destroy();
header('Location: /login.php');
exit;

この実装では <img src="/logout.php"><a href="/logout.php"> 経由でセッションが破棄される。


POST による実装例

<?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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment