php2shell をきっかけに WordPress そのものも手を入れた


前回は WordPress を運用する nginx 側の設定を見直したが、WordPress そのものも手を入れたので今回はその内容について書いておく。

大前提として

大前提の大前提だが、WordPress 本体とプラグインの随時更新は必須になる。

当ブログのような個人サイトなら多少動かなくても問題はないが、業務で WordPress を使っている場合は

  • カスタマイズテンプレート
  • 独自のプラグイン

といったものは WordPress や PHP のバージョンアップに随時対応できるよう、必ず保守契約を結んでおく必要がある。保守契約できないのであれば、静的 HTML を置くだけにしておいた方がいい。

それでも WordPress を使い続けるには

それでも WordPress を使い続けると決め、随時バージョンアップを続けるとなった場合、前回の内容は最低限やった方がいい。

いわゆるレンタルサーバだと前回の内容そのものが対応できないので、コンテナ化なり仮想サーバ化なりする必要がある。できないのであれば、WordPress を諦める選択肢も検討した方がいいだろう。

さらに WordPress 側もある程度手を入れる必要がある。ログイン周りはできるだけセキュアにした方がいいので、今回はその内容を紹介する。

二段階認証の導入

まず WordPress アカウントに二段階認証を導入する。Jetpack を導入して WordPress アカウントに移行している場合はここは対応しなくてもすでに WordPress アカウント側で対応されている。

WordPress アカウントを使わない場合は、Two Factor プラグインをインストールする。

Two Factor プラグインの設定で「認証アプリ」にチェックが入っていることを確認し、入っていない場合はチェックを入れて保存する。

次に、個人プロフィール画面の下の方に「2要素認証の設定」があるので、そこに表示されている QR コードを Google Authenticator などのスマホ認証アプリで読み込む。

Google Authenticator の場合は右下の「+」から「QR コードをスキャン」すればよい。

QR コードをスキャンすると WordPress の項目が追加されるので、そこに表示されている 6 桁の数字を WordPress 側の 3. にあるフォームに入力する。

数字は毎分 0 秒と 30 秒に更新されるので、時刻を秒単位で見つつ入力する必要がある。

「認証アプリを有効化」にチェックが入れば設定は完了となる。

API の認証必須化

WordPress の API はいくつかあるが、ユーザ一覧取得 API 経由でユーザの slug = WordPress にログインするための ID が漏えいしてしまう。つまり、WordPress への不正アクセスを防ぐ一つの鍵がないに等しい。

よって、slug を API 経由で取得できないようにする必要がある。

slug の確認

WordPress にログインしていない状態で https://ホスト名/wp-json/wp/v2/users にアクセスしてみて、ユーザ ID を含む文字列が落ちてくる。この状態が上で指摘した内容になる。

運用上、この情報はログインしている場合のみ取れればいいはずなので、認証を通すように WordPress を修正する。

ログインしていないと slug が取得できないようにする

以下内容を WordPress のディレクトリ以下にある wp-content/mu-plugins ディレクトリに置く。mu-plugins はデフォルトだと存在しないこともあるので、存在しない場合は別途作成する。

<?php
// REST の users 読み取りを、匿名のときだけ 401 で拒否(ルートは残す)
add_filter('rest_endpoints', function ($endpoints) {
    foreach (['/wp/v2/users', '/wp/v2/users/(?P<id>[\d]+)'] as $route) {
        if (empty($endpoints[$route])) {
            continue;
        }
        foreach ($endpoints[$route] as $i => $handler) {
            $methods = isset($handler['methods']) ? $handler['methods'] : [];
            $is_get  = is_array($methods)
                ? !empty($methods['GET'])
                : (false !== stripos((string) $methods, 'GET'));
            if (!$is_get) {
                continue; // 書き込み系は元々要権限なので触らない
            }
            $original = isset($handler['permission_callback'])
                ? $handler['permission_callback'] : '__return_true';
            $endpoints[$route][$i]['permission_callback'] = function ($request) use ($original) {
                if (!is_user_logged_in()) {
                    return new WP_Error('rest_forbidden', 'Authentication required.', ['status' => 401]);
                }
                return is_callable($original) ? call_user_func($original, $request) : true;
            };
        }
    }
    return $endpoints;
});

// ?author=N による列挙を未ログイン時は 403
add_action('parse_request', function () {
    if (!is_user_logged_in() && isset($_GET['author']) && is_numeric($_GET['author'])) {
        status_header(403);
        exit;
    }
});
// author アーカイブ直アクセスも未ログインは 403
add_action('template_redirect', function () {
    if (is_author() && !is_user_logged_in()) {
        status_header(403);
        exit;
    }
});

ファイル名は何でもいいが、.php で終わるようにしておく。当サイトの場合は harden-enumeration.php とした。

slug が取れないことを確認する

ファイルをアップロードしたら、まずはサイトが問題なく表示されるかを確認する。

問題なく表示されれば、WordPress にログインしていない状態で https://ホスト名/wp-json/wp/v2/users にアクセスする。以下が表示されれば ok。

{"code":"rest_forbidden","message":"Authentication required.","data":{"status":401}}

次に、WordPress にログインし、既存の記事の「投稿者」をクリックし、以下のようにプルダウンメニューが表示され、かつ投稿者全員が表示されるかどうかを確認する。

表示されれば完了となる。


2026年10月時点では概ねここまでやれば少しはマシになるだろう。

PR