WordPress ユーザーの一覧表示対策
更新日:2025年04月24日
作成日:2019年01月18日
ユーザー情報が漏れるリスク
WordPressでは、例えば「サイトのURL/?author=1」などのURLにアクセスすることで、投稿者のユーザー名(ログインID)が閲覧者に知られてしまう可能性があります。
表示されるのは厳密には「user_nicename」という項目で、「user_login」(ログイン時に使用するID)とは異なる項目ですが、初期設定のままだと「user_login」がそのまま「user_nicename」として使用されているため、実質的にログインIDが外部に判明してしまうケースが少なくありません。
また、WP REST API がデフォルトで有効化されているため、認証なしでもユーザー情報などのデータが外部から取得できる状態になっています。
ユーザー名が知られるだけで即座に大きな被害が出るわけではありませんが、ブルートフォース攻撃や辞書攻撃の成功率を高める要因になります。
そのため、複雑なパスワードの設定や、ログイン試行制限の導入(例:Limit Login Attempts Reloaded プラグイン)など、一覧表示対策以外の基本的なセキュリティ対策と組み合わせることが重要です。
対策方法
以下のいずれかの(または両方の)方法を導入することで、author アーカイブによるリスクの基本的な対策が可能です。WP REST API に対しては必要に応じて REST API を無効にするなど別途対応します。
- 著者アーカイブページへのアクセスを制御する
- user_nicename をログイン ID(user_login)とは異なるものに変更する
著者アーカイブページへのアクセスを制御する場合、以下の2つのケースが考えられます。
- 著者アーカイブページが必要ない
- 「?author=1」も著者アーカイブページ(/author/username/)も両方ブロック(完全に非公開にする)
- 著者アーカイブページを公開する
- 「?author=1」のようなアクセスはブロックし、著者アーカイブページ(/author/username/)は公開
user_nicename をログイン ID とは異なるものに変更する
ユーザー作成時の初期設定では user_nicename に user_login(ログイン ID)と同じ値が自動的に設定されているため、user_nicename をログイン ID(user_login)とは異なるものにすることで、投稿者アーカイブの URL にログイン ID が表示されるリスクを回避できます。
但し、この操作は、ユーザー作成直後に変更するのが最も安全です。運用後に変更する場合は、既存のリンクは 404 になるので、リダイレクト対応なども検討する必要があります(SEO や既存リンク対策)。
user_nicename をログイン ID(user_login)と異なるものにするには以下のような方法があります。
- データベースを直接編集
- プラグインを利用
- functions.php で変更
- ユーザー編集画面で変更(functions.php に記述)
データベースを直接編集
phpMyAdmin などを使用してデータベースを直接編集して user_nicename を変更することができます。
wp_users テーブルにある user_nicename フィールドを編集します。
該当ユーザーの行を探し、user_nicename の値を任意の文字列(ログイン ID と異なる値)に変更します。
注意: URL 構造(example.com/author/スラッグ)にも影響するため、スラッグとして適切な文字列を設定する必要があります(半角英数字、ハイフン推奨)。
プラグインを利用
専用のユーザー編集プラグイン(例:Edit Author Slug)を使えば、user_nicename に相当する「著者スラッグ」を GUI から安全に変更できます。
この方法はデータベースを直接編集するよりも安全かつ便利です。
functions.php で変更
wp_update_user() 関数などを使って変更することも可能です。
以下は functions.php で wp_update_user() を使って、ユーザーID が 1 のユーザーの user_nicename を変更する例です。以下のコードは一度アクセスするだけで実行される(ページを開くたびに毎回実行される)ので、実行後は必ず add_action() をコメントアウトします。
user_nicename はスラッグ(URLに使う文字列)なので、記号などを含めるとトラブルの元になるため、必ず sanitize_title_with_dashes() や sanitize_title() を通して英数字・ハイフンだけに整えます。
function safe_update_user_nicename_on_init() {
$user_id = 1; // 対象ユーザーID
$new_nicename = 'foo-public'; // 新しい user_nicename
$slug = sanitize_title_with_dashes($new_nicename);
// すでに同じスラッグが使われていないか確認
$existing_user = get_user_by('slug', $slug);
if ($existing_user && $existing_user->ID != $user_id) {
error_log('すでに使われているスラッグのため、更新を中止しました。');
return;
}
// 更新
$result = wp_update_user([
'ID' => $user_id,
'user_nicename' => $slug
]);
if (is_wp_error($result)) {
error_log('ユーザー更新に失敗しました: ' . $result->get_error_message());
} else {
error_log('ユーザーID ' . $user_id . ' の user_nicename を ' . $slug . ' に更新しました。');
}
}
// 実行が完了したら以下の add_action() をコメントアウト
add_action('init', 'safe_update_user_nicename_on_init');
確認
確認方法としては、phpMyAdmin などでデータベースを開いて、wp_users テーブルの user_nicename カラムの値を直接確認するのが確実です。
/author/{新しいuser_nicename} にアクセスして確認することもできます。新しい user_nicename で著者アーカイブが表示されれば成功です(旧スラッグでアクセスすると 404 にります)。
但し、サーバーやブラウザキャッシュにより、すぐに新しい user_nicename が反映されないこともあるため確認するときは、ブラウザキャッシュをクリアするか、シークレットモードでチェックすると確実です。
また、サーバーの error_log を確認することもできます(上記のコードでは成功でも失敗でもログを出力)。
error_log() により出力されるサーバーの error_log ファイルの場所は、環境によって異なります。例えば、MAMP/XAMPP(ローカル開発環境)の場合、以下にあります。
- MAMP: /Applications/MAMP/logs/php_error.log
- XAMPP: xampp/php/logs/php_error_log
また、開発環境の場合、WordPress 自体のデバッグ設定(wp-config.php)で以下が設定されていれば、/wp-content/debug.log に出力されます(※ 本番環境ではこれらは false にします)。
但し、WP_DEBUG_LOG が true になっている場合、WordPress が PHP エラーや error_log() の出力をキャッチし、wp-content/debug.log に記録するため、通常のサーバーの error_log(MAMP の場合は php_error.log など)には出力されません。
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
複数ユーザーの user_nicename 一括更新
以下は複数ユーザーの user_nicename 一括更新する例です。$users_to_update に、更新対象のユーザーIDと新しい user_nicename をセットします。
この場合も、前述のコード同様、実行後は必ず add_action() をコメントアウトします。
function bulk_update_user_nicenames() {
// 更新したいユーザーのリストを用意(ユーザーID と新しい user_nicename を指定)
$users_to_update = [
// user_id => 新しい user_nicename
2 => 'alice-public',
3 => 'bob-public',
4 => 'charlie-public',
// 必要に応じて追加
];
foreach ($users_to_update as $user_id => $new_nicename) {
$slug = sanitize_title_with_dashes($new_nicename);
// すでに同じスラッグが存在するか確認
$existing_user = get_user_by('slug', $slug);
if ($existing_user && $existing_user->ID != $user_id) {
error_log("スラッグ '{$slug}' は既にユーザーID {$existing_user->ID} が使用しています。スキップしました。");
continue;
}
// スラッグを更新
$result = wp_update_user([
'ID' => $user_id,
'user_nicename' => $slug,
]);
if (is_wp_error($result)) {
error_log("ユーザーID {$user_id} の更新に失敗しました: " . $result->get_error_message());
} else {
error_log("ユーザーID {$user_id} の user_nicename を '{$slug}' に更新しました。");
}
}
}
// 実行が完了したら以下の add_action() をコメントアウト
add_action('init', 'bulk_update_user_nicenames');
ユーザー編集画面で変更
functions.php に以下を記述して、ユーザー編集画面で user_nicename を変更することもできます。
show_user_profile と edit_user_profile アクションでプロフィール画面に独自の nicename フィールドを追加し、personal_options_update と edit_user_profile_update で値を保存します。
プロフィール画面に独自の nicename フィールドを追加する際は、「ニックネーム」と「ブログ上の表示名」の間に表示するように、フィールドを追加後に JavaScript を使って、要素を移動しています。また、念の為、エラー表示のスタイルをクリアします。
保存の際は、指定された nicename が既存ユーザーと重複している場合は user_profile_update_errors アクションを使ってエラーを表示し(保存を中断して)、現在(これまで)の nicename を復元します。
/**
* ユーザー編集画面に nicename フィールドを追加する
*
* @param WP_User $user 現在編集しているユーザーオブジェクト
*/
function my_add_nicename_field($user) {
// ユーザーデータを取得
$userdata = get_userdata($user->ID);
?>
<table class="form-table" id="my-nicename-field">
<tr>
<th><label for="user_nicename">ナイスネーム(nicename)</label></th>
<td>
<input name="user_nicename" id="user_nicename" value="<?php echo esc_attr($userdata->user_nicename); ?>" class="regular-text" />
<span class="description">ユーザー名と異なる値を指定</span>
</td>
</tr>
</table>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 出力された nicename フィールドを、JavaScriptで「ニックネーム」と「ブログ上の表示名」の間に移動
// ニックネーム(nickname)フィールドの input 要素を探す
const nicknameInput = document.getElementById('nickname');
if (!nicknameInput) return; // 見つからなければ終了
// ニックネームフィールドの <tr> 要素を取得
const nicknameRow = nicknameInput.closest('tr');
// nicename フィールドの <tr> 要素を取得
const nicenameRow = document.querySelector('#my-nicename-field tr');
// ニックネームフィールドの直後に nicename フィールドを挿入
if (nicknameRow && nicenameRow) {
nicknameRow.parentNode.insertBefore(nicenameRow, nicknameRow.nextSibling);
}
// nicename フィールドのスタイルとエラーをリセット
const nicenameInput = document.getElementById('user_nicename');
if (nicenameInput) {
nicenameInput.style.backgroundColor = ''; // 背景色をリセット
nicenameInput.style.padding = '6px';
const existingError = document.querySelector('.nicename-error-message');
if (existingError) existingError.remove(); // もしエラーメッセージが残ってたら消す
}
});
</script>
<?php
}
// ユーザー自身がプロフィールを見る・編集する画面で nicename フィールドを表示
add_action('show_user_profile', 'my_add_nicename_field');
// 管理者が他人のプロフィールを編集する画面でも nicename フィールドを表示
add_action('edit_user_profile', 'my_add_nicename_field');
/**
* ユーザーの nicename を保存・更新する
*
* @param int $user_id 保存対象のユーザーID
*/
function my_update_nicename($user_id) {
// 現在のユーザーがこのユーザーを編集できるか確認
if (! current_user_can('edit_user', $user_id)) {
return false;
}
// nicename フィールドが送信されているかチェック
if (! isset($_POST['user_nicename'])) {
return;
}
// 入力された nicename を正規化(スラッグ化)
$nicename = sanitize_title_with_dashes($_POST['user_nicename']);
// すでに同じ nicename を持つ他のユーザーがいないかチェック
$existing_user = get_user_by('slug', $nicename);
// nicename が既存ユーザーと重複している場合はエラーを表示
if ($existing_user && $existing_user->ID != $user_id) {
// エラー登録 ( $errors->add() でエラーを登録し、エラーメッセージを出して、保存を止める)
add_action('user_profile_update_errors', function ($errors) use ($nicename) {
$errors->add(
'user_nicename_exists',
sprintf(
'ナイスネーム「%s」はすでに使用されています。別のものを指定してください。',
esc_html($nicename)
)
);
// エラー時に JavaScript を出力(admin_footer で JS を設定)
add_action('admin_footer', function () use ($nicename) {
?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const nicenameInput = document.getElementById('user_nicename');
if (!nicenameInput) return;
// エラーが出ていなければエラーメッセージを追加
if (!document.querySelector('.nicename-error-message')) {
// 入力フィールドに背景色をつける
nicenameInput.style.backgroundColor = '#fae3ec';
// エラーメッセージを作成
const errorMessage = document.createElement('div');
errorMessage.innerHTML = 'ナイスネーム「<?php echo esc_html($nicename); ?>」はすでに使用されています。<br>以前のナイスネームに復元しました。<br>変更する場合は新しい値を入力してください。<br>変更しない場合も必ず「ユーザーを更新」ボタンをクリックしてください。';
errorMessage.style.color = 'red';
errorMessage.style.marginTop = '8px';
errorMessage.style.lineHeight = '1.5';
errorMessage.classList.add('nicename-error-message');
// エラーメッセージをフィールドの下に追加
nicenameInput.insertAdjacentElement('afterend', errorMessage);
// フィールドへスムーズスクロール
nicenameInput.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
});
</script>
<?php
});
});
// エラー時は現在の nicename を復元
$current_userdata = get_userdata($user_id);
$nicename = $current_userdata->user_nicename;
}
// nicename を更新
$userdata = array(
'ID' => $user_id,
'user_nicename' => $nicename,
);
wp_update_user($userdata);
}
// ユーザー自身がプロフィールを保存する時に nicename を保存
add_action('personal_options_update', 'my_update_nicename');
// 管理者が他人のプロフィールを保存する時も nicename を保存
add_action('edit_user_profile_update', 'my_update_nicename');
上記を保存して、ユーザー編集画面を開くと「ニックネーム(必須)」と「ブログ上の表示名」の間にナイスネームのフィールドが追加され、user_nicename を設定できるようになります。
設定した nicename が既存ユーザーの user_nicename と重複している場合はエラーを表示し、入力された値はクリアされ元の値が復元されます。
上記の状態で、ページを再読込すると、「無効なユーザー ID です」とだけ表示されるので、エラーメッセージには「必ず更新ボタンをクリックしてください」と表示しています。もし、「無効なユーザー ID です」とだけ表示された場合は、ブラウザの「戻る」ボタンをクリックする必要があります。
使用しているアクションフック
アクション | 説明 |
---|---|
show_user_profile | 自分自身のプロフィールページ(/wp-admin/profile.php)を表示するときに呼ばれるアクション。自分のプロフィール画面に独自のフィールドを追加したいときなどに使用。 |
edit_user_profile | 他のユーザーのプロフィールページ(/wp-admin/user-edit.php)を表示するときに呼ばれるアクション。管理者が「他のユーザーのプロフィールを編集」する画面で、独自のフィールドを追加したいときに使用。 |
personal_options_update | 自分自身のプロフィールページを保存するときに呼ばれるアクション。フォーム送信時に「自分の独自のフィールドの値を保存する」処理を記述。 |
edit_user_profile_update | 他のユーザーのプロフィールページを保存するときに呼ばれるアクション。管理者などが「他人のプロフィールを編集して保存」したときに、独自のフィールドの値を保存する処理を記述。 |
user_profile_update_errors | ユーザーのプロフィール(ユーザー情報)を更新しようとしたときに、バリデーションエラー(入力エラー)を追加するために使うアクション。$errors というエラーオブジェクト(WP_Error)を受け取り、この $errors にエラーを追加すると、保存がストップし、画面上にエラーメッセージが表示される |
admin_footer | 管理画面(ダッシュボード)で、HTMLの</body>タグ直前にフックされるアクション。管理画面の一番下にHTMLやJSを追加したいときに使うフック |
WP REST API を無効にする
WordPress 4.7 から「WP REST API」がデフォルトで有効になっています。そのため認証無しでも投稿記事やユーザー情報などのデータを外部から取得する事ができるようになっています。
例えば、以下のアドレスにアクセスしてみると、json 形式のユーザーのデータが表示されます。
「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」
REST APIを無効化することで、匿名の外部アクセスを防止できますが、すべて無効化すると、REST API に依存する WordPress 管理画面の機能(Gutenberg エディターなど)が動作しなくなります。
そのため、無効にする場合は、以下のように rest_authentication_errors フィルターを使って、ログインしていない場合にのみ無効にする方法が推奨されています(REST API Handbook )。
// ログインしていない場合、REST API へのアクセスを制限する
add_filter('rest_authentication_errors', function ($result) {
// 以前の認証チェックが適用されている場合は、その結果を変更せずに渡す(返す)
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
// ログイン済みユーザーは許可
if (is_user_logged_in()) {
return $result;
}
// ログインしていない場合はエラーを返す
return new WP_Error(
'rest_not_logged_in',
__('You are not currently logged in.', 'text-domain'),
array('status' => 401)
);
});
但し、この方法だと、ユーザーがログインしていないと Contact Form7 などの REST API を使用しているプラグインが機能しなくなってしまいます。
特定 API(プラグイン)だけ例外許可
前述のコードの場合、ログインしていないリクエストに対して無条件に WP_Error を返して、REST API を強制的に止めているため、Contact Form 7 などの「ログインしていないユーザー」も対象にして REST API 通信しているプラグインは正常動作できなくなってしまいます。
そのため、プラグインが使う特定のエンドポイントだけを許可する処理を追加する必要がありますが、以下のいずれかの方法で対応可能です(他にも方法はあるかと思います)。
rest_authentication_errors を利用
以下は rest_authentication_errors フィルターを使って、Embed(oEmbed)や Contact Form 7、 Akismet プラグイン、及びカスタムエンドポイントを許可する例です。
許可するルート($allowed_routes)には、REST APIのエンドポイントの /wp-json/ 以降、最初のパス(namespace)にスラッシュを付けた形で指定します(例:/contact-form-7/v1 など)。
add_filter('rest_authentication_errors', function ($result) {
if (true === $result || is_wp_error($result)) {
return $result;
}
// ログイン済みユーザーは許可(管理画面の機能を使えるように)
if (is_user_logged_in()) {
return $result;
}
// REST API リクエストルートを取得
global $wp;
// /wp-json/ 以降のパスが取れる(例: /contact-form-7/v1 や /wp/v2/posts など)
$rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';
// 許可するルート
$allowed_routes = [
'/oembed/1.0', // Embed 用
'/contact-form-7/v1', // Contact Form 7 用
'/akismet/v1', // Akismet 用
'/my-theme-api/v1/posts', // カスタムエンドポイント 用
];
// 対象のルートは許可
foreach ($allowed_routes as $allowed_route) {
if (strpos($rest_route, $allowed_route) === 0) {
return $result; // 許可対象ならOK
}
}
// それ以外はエラーを返す
return new WP_Error(
'rest_not_logged_in',
__('You are not currently logged in.', 'text-domain'),
array('status' => 401)
);
});
上記コードでは、ログイン済みユーザーはすべて許可していますが、 投稿・固定ページの編集権限があるユーザーのみ許可(例: Gutenberg使用者)する場合は7-9行目を以下のように書き換えます。
if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
return $result;
}
上記のコードにより、ログインユーザー以外は、「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」などにアクセスしても、許可したルート以外は表示されません。
rest_pre_dispatch を利用
以下は、rest_pre_dispatch フィルターを使って、引数 $request の get_route() メソッドをで、ルート判定して特定 API(プラグイン)だけ例外許可する例です。
前述の rest_authentication_errors を使ったコード同様、特定のプラグイン(oEmbed, Contact Form 7, Akismet)だけ許可し、あとは制限(エラー)します。許可の基準は namespace になるので先頭のスラッシュなしで指定します。環境に合わせて30-34行目部分を変更します。
また、以下では編集権限があるユーザーのみ許可していますが、ユーザーがログインしていれば許可するには9行目の判定を is_user_logged_in() に変更します。
// 編集権限のあるユーザー及び指定したプラグインに REST API を許可(それ以外は無効にする)
add_filter('rest_pre_dispatch', function ($result, $server, $request) {
// 念のため、リクエストオブジェクトの型チェック ( WP_REST_Request 以外なら処理しない )
if (! ($request instanceof WP_REST_Request)) {
return $result;
}
// 投稿・固定ページの編集権限があるユーザーなら許可(例: Gutenberg使用者)
if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
return $result;
}
// ルート取得(先頭スラッシュを除去)
$route = ltrim($request->get_route(), '/');
// ルートが空の場合は REST API を無効化
if (empty($route)) {
return new WP_Error(
'rest_disabled',
__('The REST API on this site has been disabled.', 'text-domain'),
array('status' => 403)
);
}
// スラッシュでルートを分割して namespace を抽出(例: "contact-form-7/v1")
$parts = explode('/', $route);
$namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';
// 許可する namespace 一覧(oEmbed、Contact Form 7、Akismet)
$permitted_namespaces = [
'oembed/1.0', // Embed 用
'contact-form-7/v1', // Contact Form 7 用
'akismet/v1', // Akismet 用
];
// 許可リストに含まれていれば通過
if (in_array($namespace, $permitted_namespaces, true)) {
return $result;
}
// それ以外は REST API を無効化(ステータス 403)
return new WP_Error(
'rest_disabled',
__('The REST API on this site has been disabled.', 'text-domain'),
array('status' => 403)
);
}, 10, 3);
上記のコードにより、編集権限があるユーザーも含め、「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」などにアクセスしても、許可したルート以外は表示されません。
除外するプラグインを追加
除外したいプラグインが増えた場合は、上記コードの $permitted_namespaces(30-34行目)にそのプラグインのエンドポイントの名前空間を追加で指定します。
REST API を利用しているプラグインの名前空間は「サイトのURL/wp-json/」でアクセスして表示されるデータの namespaces で確認できます(REST API を有効にしておきます)。
rest_authentication_errors を使用する場合との違い
どちらのコードも「処理結果」的には、特定プラグインは許可、それ以外は制限するので同じですが、実行タイミングが異なります。
rest_authentication_errors のコードの場合、認証段階(APIアクセスのかなり早い段階)で実行されますが、rest_pre_dispatch のコードではリクエストの直前(データ取得直前)に実行されます。
/wp-json/wp/v2/users など許可したルート以外にアクセスした場合、rest_authentication_errors のコードでは、ログインユーザーはデータが表示されますが、rest_pre_dispatch のコードでは、編集権限があるユーザーでも表示されません。
この例の場合、rest_authentication_errors のコードでは 401 Unauthorized を返していますが、rest_pre_dispatch のコードでは 403 Forbidden を返しています。
「未ログインユーザー」に厳しく制限したい(API認証時にすぐ弾きたい)場合は rest_authentication_errors を使用し、「一部権限のあるユーザー(編集者など)」にも許可しつつ、柔軟に制御したい場合は rest_pre_dispatch を使うなどが考えられます。