phpinfo() にパスワード認証を追加する

アフィリエイト広告を利用しています

このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です

phpinfo() は、PHP のバージョンや拡張機能、各種設定を確認できる便利な関数です。サーバー環境の調査やトラブルシューティングで使用することがありますが、サーバー内部の情報が詳しく表示されるため、確認が終わったら速やかにファイルを削除するのが原則です。

とはいえ、作業中に別の対応が入ったり、確認後に削除するのをうっかり忘れてしまったりすることもあります。パスワード認証なしで設置したままだと、ファイルの URL を知った第三者にもサーバー情報が表示されてしまいます。

そこで、削除し忘れた場合の安全策として、phpinfo() にパスワード認証を追加します。万が一ファイルを残してしまっても、認証されていないアクセスに対しては環境情報を表示しない仕組みです。

あくまで「削除し忘れたときのリスクを軽減するための保険」であり、パスワード認証を追加したからといって、phpinfo() を設置したままにしてよいわけではありません。

パスワード認証付き phpinfo() のサンプルコード

以下は、パスワード認証と有効期限付き Cookie を組み合わせたサンプルです。認証後は PHP の主な環境情報を表形式で表示し、その下に phpinfo() の詳細情報を表示します。

<?php

define('ACCESS_PASSWORD', '★★★★');
define('ACCESS_COOKIE_NAME', 'php_env_auth');

date_default_timezone_set('Asia/Tokyo');

$now = time();
$authenticated = false;
$accessExpiresAt = null;
$cookiePath = $_SERVER['SCRIPT_NAME'] ?? '/';
$cookieOptions = [
    'path' => $cookiePath,
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
];

$allowedMinutes = [0, 10, 30, 60, 120];

// パスワードと設置先の絶対パスを署名鍵として使用する。
$signingKey = hash('sha256', ACCESS_PASSWORD . "\0" . __FILE__, true);

// 次回以降のアクセスは、Cookie の期限と HMAC 署名を検証する。
$cookieValue = $_COOKIE[ACCESS_COOKIE_NAME] ?? null;

if (is_string($cookieValue)) {
    if (preg_match('/\A([0-9]{1,12})\.([0-9]{1,12})\.([0-9a-f]{64})\z/', $cookieValue, $matches) === 1) {
        $issuedAt = (int) $matches[1];
        $expiresAt = (int) $matches[2];
        $signature = $matches[3];
        $durationSeconds = $expiresAt - $issuedAt;
        $expectedSignature = hash_hmac('sha256', $issuedAt . '.' . $expiresAt, $signingKey);

        if (hash_equals($expectedSignature, $signature)) {
            if (in_array($durationSeconds, [600, 1800, 3600, 7200], true)) {
                if ($issuedAt <= $now) {
                    if ($now < $expiresAt) {
                        $authenticated = true;
                        $accessExpiresAt = $expiresAt;
                    }
                }
            }
        }
    }
}

// Cookie が無効な場合のみパスワードで認証する。
if (!$authenticated) {
    $postedPassword = filter_input(INPUT_POST, 'key');

    if (is_string($postedPassword)) {
        if (hash_equals(ACCESS_PASSWORD, $postedPassword)) {
            $authenticated = true;
            $rememberMinutes = filter_input(INPUT_POST, 'remember_minutes', FILTER_VALIDATE_INT);

            if (!in_array($rememberMinutes, $allowedMinutes, true)) {
                $rememberMinutes = 0;
            }

            if ($rememberMinutes > 0) {
                $issuedAt = time();
                $accessExpiresAt = $issuedAt + $rememberMinutes * 60;
                $payload = $issuedAt . '.' . $accessExpiresAt;
                $signature = hash_hmac('sha256', $payload, $signingKey);
                $cookieOptions['expires'] = $accessExpiresAt;

                setcookie(ACCESS_COOKIE_NAME, $payload . '.' . $signature, $cookieOptions);

                // POST の再送信を避け、次の GET から Cookie で認証する。
                header('Location: ?', true, 303);
                exit;
            }
        }
    }
}

header('Cache-Control: private, no-store');

if (!$authenticated) {
    ?>
        <form action="?" method="post">
            <input type="password" name="key" autocomplete="current-password" autofocus>
            <label for="remember_minutes">記録</label>
            <select name="remember_minutes" id="remember_minutes">
                <option value="0">しない</option>
                <option value="10">10分</option>
                <option value="30">30分</option>
                <option value="60">60分</option>
                <option value="120">120分</option>
            </select>
            <input type="submit" value="送信">
        </form>
    <?php
    exit;
}

$rows = [
    'PHPINFO アクセス期限' => $accessExpiresAt === null ? '記録しない(今回のみ)' : date('Y-m-d H:i:s', $accessExpiresAt),
    '現在日時' => date('Y-m-d H:i:s'),
    'PHP バージョン' => PHP_VERSION,
    'ZIP' => extension_loaded('zip') ? 'ON' : 'OFF',
    'ZipArchive' => class_exists('ZipArchive') ? 'ON' : 'OFF',
    'extension_dir' => ini_get('extension_dir'),
    'disable_classes' => ini_get('disable_classes'),
    'php.ini' => php_ini_loaded_file(),
    'timezone' => ini_get('date.timezone'),
    'mysqli socket' => ini_get('mysqli.default_socket'),
    'pdo_mysql socket' => ini_get('pdo_mysql.default_socket'),
    'Current PHP version' => phpversion(),
];

?>
<div class="php-env-report">

    <style>
        .php-env-report {
            max-width: 960px;
            margin: 20px auto;
            font-family: sans-serif;
            font-size: 14px;
            line-height: 1.6;
            color: #333;
        }

        .php-env-report .report-title {
            margin: 0 0 15px;
            padding: 10px 15px;
            background: #327c3c;
            color: #fff;
            font-size: 20px;
            font-weight: bold;
        }

        .php-env-report table {
            width: 100%;
            border-collapse: collapse;
            table-layout: fixed;
        }

        .php-env-report th,
        .php-env-report td {
            padding: 10px 12px;
            border: 1px solid #ccc;
            text-align: left;
            vertical-align: top;
            overflow-wrap: anywhere;
        }

        .php-env-report th {
            width: 35%;
            background: #f0f0f0;
            font-weight: bold;
        }

        .php-env-report tr:nth-child(even) td {
            background: #f8f8f8;
        }

        .php-env-report .status-on {
            color: #16803c;
            font-weight: bold;
        }

        .php-env-report .status-off {
            color: #c62828;
            font-weight: bold;
        }

        .php-env-report .report-empty {
            color: #999;
        }
    </style>

    <div class="report-title">PHP 環境確認</div>

    <table>
        <tbody>
            <?php foreach ($rows as $label => $value) { ?>
                <?php
                    $class = '';

                    if ($value === 'ON') {
                        $class = 'status-on';
                    }

                    if ($value === 'OFF') {
                        $class = 'status-off';
                    }

                    if ($value === false || $value === '') {
                        $value = '(未設定)';
                        $class = 'report-empty';
                    }
                ?>
                <tr>
                    <th><?php echo htmlspecialchars($label, ENT_QUOTES, 'UTF-8'); ?></th>
                    <td class="<?php echo $class; ?>">
                        <?php echo htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); ?>
                    </td>
                </tr>
            <?php } ?>
        </tbody>
    </table>

</div>
<?php if ($accessExpiresAt !== null) { ?>
<script>
    const accessExpiresAt = <?php echo $accessExpiresAt * 1000; ?>;
    const remainingMilliseconds = Math.max(0, accessExpiresAt - Date.now());

    setTimeout(() => {
        window.location.replace(window.location.pathname + window.location.search);
    }, remainingMilliseconds);
</script>
<?php } ?>
<?php

phpinfo();

?>

設置方法

上記のコードを phpinfo.php などの名前で保存し、PHP が実行できるサーバーに配置します。

ACCESS_PASSWORD は掲載用に ★★★★ としているため、実際に使用する際は必ず推測されにくい独自のパスワードに変更してください。また、Cookie の Secure 属性を使用しているため、HTTPS 環境で利用します。

ブラウザからファイルにアクセスすると、パスワード入力フォームが表示されます。正しいパスワードを入力すると、PHP の環境情報と phpinfo() の内容が表示されます。

確認作業が完了したら、パスワード認証の有無にかかわらず、設置した PHP ファイルを速やかに削除してください。

コードのポイント解説

1. パスワード認証を通過するまで phpinfo() を表示しない

ACCESS_PASSWORD に設定したパスワードと、フォームから送信された値を hash_equals() で比較します。認証されていない場合はパスワード入力フォームだけを表示し、exit で処理を終了します。

これにより、ファイルを削除し忘れた場合でも、パスワードを知らない第三者に対しては phpinfo() の内容を表示しないようにしています。

2. Cookieで認証状態を一定時間保持する

認証時には「記録しない」「10分」「30分」「60分」「120分」から認証状態の保持時間を選択できます。短時間に何度も環境情報を確認する場合、毎回パスワードを入力する手間を省けます。

「記録しない」を選択した場合は Cookie を発行しないため、ページを再読み込みすると再びパスワードの入力が必要です。

3. HMAC署名でCookieの改ざんを検出する

Cookie には発行時刻と有効期限に加え、hash_hmac() によって生成した署名を保存します。次回アクセス時には、署名と有効期限を検証して認証状態を判定します。

Cookie には Secure、HttpOnly、SameSite=Lax を設定しています。なお、これらは認証情報の取り扱いを補助する設定であり、公開されたファイルを完全に保護するものではありません。

4. PHPの環境情報を一覧表示する

認証後は、PHP バージョン、ZIP 拡張機能、ZipArchive、php.ini の場所、タイムゾーン、MySQL 関連の設定などを表形式で表示します。

その下には phpinfo() の詳細情報も表示するため、一覧表で主な項目を確認し、必要に応じてさらに詳しい設定値を調べることができます。

5. 認証の有効期限に合わせて再読み込みする

Cookie に有効期限を設定した場合は、JavaScript の setTimeout() を使って期限到達時にページを再読み込みします。期限切れの Cookie は PHP 側でも認証されないため、再読み込み後は再びパスワード入力フォームが表示されます。

実務で注意すべきポイント

今回のパスワード認証は、削除し忘れた場合のリスクを軽減するための安全策です。パスワード認証があっても、ファイルを設置したままにするリスクがなくなるわけではありません。

  • 確認が終わったら必ず削除する:パスワード認証は削除の代わりにはならない
  • 推測されにくいパスワードを設定する:掲載用の ★★★★ のまま使用しない
  • HTTPS環境で使用する:パスワードや認証情報が通信中に漏れないようにする
  • 必要に応じてアクセス元を制限する:公開サーバーでは IP 制限や Web サーバー側の認証を併用すると、さらにリスクを軽減できる
  • パスワードの総当たり攻撃に注意する:このサンプルには認証失敗回数の制限がないため、パスワード認証だけで十分とは考えない
  • 設置したファイルの存在を忘れない:一時的な調査用ファイルとして扱い、作業終了時に削除を確認する

最も確実なのは、phpinfo() を含むファイルをサーバーから削除することです。パスワード認証は、あくまで削除を忘れてしまった場合に備える追加の防御策として考えてください。

まとめ

phpinfo() は PHP の実行環境を調べるうえで便利ですが、サーバーの詳細な情報を表示するため、確認が終わったら速やかに削除する必要があります。

今回のサンプルでは、万が一ファイルの削除を忘れてしまった場合に備え、パスワード認証を追加しました。これにより、認証されていない第三者からのアクセスに対して、環境情報がそのまま表示されることを防ぎます。

ただし、パスワード認証は「削除しなくてもよい仕組み」ではなく、「削除を忘れたときのための保険」です。 必要なときだけ設置し、確認後はすぐに削除するという基本的な運用を前提として、補助的な安全策として活用してください。