はじめに
このブログは2020年から WordPress で運用しています。コメントスパム、検索結果を悪用したSEOスパム、管理画面に入れなくなるトラブルなど、何かが起きるたびに対策を足してきました。
この記事では、その積み重ねを「WordPressを構築したら最初にやるセキュリティ設定」として、まとめ直します。
これまでの記事
| 時期 | 記事 | この記事で関係する章 |
|---|---|---|
| 2020年5月 | WordPressはスパム対策だけで十分か | STEP 4(コメントスパム) |
| 2022年11月 | BackWPup4.0で重大なエラーが発生していたので調べてみた | STEP 2(バックアップ) |
| 2025年10月 | WordPressにJetpackを導入してMarkdownで記事を書けるようにしてみた | STEP 5(Jetpackとの両立) |
| 2026年2月 | WordPressの検索結果を悪用したSEOスパム攻撃|事象まとめと対処 | STEP 4(検索結果ページ) |
| 2026年9月 | WordPressの管理画面に入れなくなったので乗っ取りを疑って切り分けてみた | STEP 3・4(プラグインの重ねがけ、ログインURLの漏れ) |
方針
- 攻撃者と同じく「外」から見て、見えているものを塞ぐ
- セキュリティ系のプラグインは重ねすぎない(放置されたプラグインのせいで、自分が締め出されたことがあります)
- Jetpack を使うなら、XML-RPC と REST API は開けておく。その代わりに、別の対策を入れる
- 最後に WPScan で外から確認し、定期的に見直す
環境
| 項目 | 内容 |
|---|---|
| サーバー | エックスサーバー |
| WordPress | 7.x |
| テーマ | Cocoon(子テーマ) |
| セキュリティ | SiteGuard WP Plugin、エックスサーバーの WAF とWordPressセキュリティ設定 |
| その他 | Jetpack(Markdown で記事を書くため) |
STEP 1. まず外から見る
攻撃者は、まずサイトの外から「ログインURL」「ユーザー名」「開いている入口」「置きっぱなしのファイル」を調べます。同じことを自分でもやってみます。
次のスクリプトは、GET と、XML-RPC に「使える機能の一覧」を問い合わせるリクエストだけを送ります。必ず自分のサイトに対して実行してください。
#!/usr/bin/env bash
# 使い方: bash wp-outside-check.sh https://example.com
B=${1%/}
code() { curl -s -o /dev/null -w "%{http_code} %{redirect_url}" --max-time 15 "$B/$1"; }
echo "== ログインURLが漏れていないか =="
for p in wp-login.php wp-admin/ login admin dashboard; do
printf "%-28s %s\n" "/$p" "$(code "$p")"
done
echo "-- ページ内のログインリンク --"
curl -sL --max-time 20 "$B/" | grep -oE 'href="[^"]*(wp-login|login_[0-9]+)[^"]*"' | sort -u
echo
echo "== ユーザー名が漏れていないか =="
printf "%-28s %s\n" "/?author=1" "$(code '?author=1')"
curl -s --max-time 15 "$B/wp-json/wp/v2/users" | grep -oE '"slug":"[^"]+"' | sed 's/^/REST API users /'
POST=$(curl -s --max-time 15 "$B/wp-json/wp/v2/posts?per_page=1&_fields=link" | sed 's#\\/#/#g' | grep -oE 'https?://[^"]+' | head -1)
curl -s --max-time 15 "$B/wp-json/oembed/1.0/embed?url=$POST" | sed 's#\\/#/#g' | grep -oE '"author_url":"[^"]+"' | sed 's/^/oEmbed /'
echo
echo "== XML-RPC =="
curl -s --max-time 15 -X POST -H 'Content-Type: text/xml' \
--data '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
"$B/xmlrpc.php" | grep -oE 'wp\.getUsersBlogs|pingback\.ping' | sort -u | sed 's/^/有効なメソッド /'
echo
echo "== 置きっぱなしのファイル =="
for p in wp-config.php.bak wp-config.php.save wp-config.php~ .env .git/config wp-content/debug.log readme.html wp-content/uploads/; do
printf "%-28s %s\n" "/$p" "$(code "$p")"
done
echo
echo "== 検索結果ページの robots =="
curl -sL --max-time 15 "$B/?s=test" | grep -oiE "<meta name=.robots.[^>]*>"
結果の読み方
| 確認項目 | 良い状態 | 対策が必要な状態 | 対策する章 |
|---|---|---|---|
/wp-login.php | 404 | 200 | STEP 3 |
/wp-admin/ | 404、またはトップページへ転送 | 変更後のログインURLへ転送される | STEP 3 |
| ページ内のログインリンク | 何も出ない | login_数字 などが出る | STEP 4 |
/?author=1 | トップページへ転送 | /author/ユーザー名/ へ転送される | STEP 3 |
| REST API users / oEmbed | ログインIDとは違う文字列 | ログインIDと同じ文字列 | STEP 5 |
| XML-RPC | pingback.ping が出ない(Jetpack を使わないなら XML-RPC 自体が止まっている) | pingback.ping が出る | STEP 3 |
| 置きっぱなしのファイル | 404 / 403 | 200 | 削除する |
| 検索結果ページ | noindex | 何も付いていない | STEP 4 |
このブログでは、SiteGuard でログインURLを変更していたにもかかわらず、ログインURLが2か所から漏れていました。
/wp-admin/を開くと、誰でも変更後のログインURLに転送されていた- サイドバーの「メタ情報」ウィジェットの「ログイン」リンクに、変更後のログインURLが全ページで出ていた
STEP 2. サーバー側の設定(エックスサーバー)
WAF設定
サーバーパネルの「WAF設定」で、すべての項目を ON にします。記事の保存やプラグインの更新が誤って弾かれたときは、SiteGuard の「WAFチューニングサポート」で、その操作だけを除外します。
WordPressセキュリティ設定
| 項目 | 設定 | 注意 |
|---|---|---|
| 国外IPアクセス制限(ダッシュボード / XML-RPC API / REST API) | ON | Jetpack からのアクセスは制限の対象外なので、Jetpack を使っていても ON でかまいません。海外から管理画面を使うときや、Cloudflare など海外を経由するサービスを使うときは OFF にする必要があります |
| ログイン試行回数制限 | ON | |
| コメント・トラックバック制限 | ON |
バックアップ
エックスサーバーは、1日1回自動でバックアップを取っています。保持されるのは、Webデータ・メール・MySQL とも過去14日分です(古いサーバーでは Web とメールが7日分)。
改ざんに気づくのが遅れると、14日分では足りないこともあります。自動バックアップだけに頼らず、プラグインなどで自分でもバックアップを取っておきましょう(参考:BackWPup4.0で重大なエラーが発生していたので調べてみた)。
STEP 3. SiteGuard WP Plugin の設定
| 機能 | 設定 | ポイント |
|---|---|---|
| 管理ページアクセス制限 | 任意 | ログインしていない接続元から /wp-admin/ を守る |
| ログインページ変更 | ON +「管理者ページからログインページへリダイレクトしない」 | チェックを入れないと、/wp-admin/ を開くだけで変更後のURLが分かる |
| 画像認証 | ON | 他の CAPTCHA 系プラグインと重ねない |
| ログイン詳細エラーメッセージの無効化 | ON | ユーザー名とパスワードのどちらが違うのかを表示しない |
| ログインロック | ON | 何度も失敗した接続元を一時的に締め出す |
| ログインアラート | ON | 自分以外のログインにすぐ気づける |
| フェールワンス | 任意 | 正しく入力しても1回目はわざと失敗する。慣れていないと「ログインできない」と焦る |
| XMLRPC防御 | Jetpack あり:ピングバック無効化 / なし:XMLRPC無効化 | XMLRPC無効化にすると Jetpack が動かなくなる |
| ユーザー名漏えい防御 | ON(Jetpack ありなら「REST API 無効化」は OFF) | 有効にしただけでは、/wp-json/wp/v2/users は開いたまま(STEP 5 で対策) |
| 更新通知 | ON | 本体・プラグイン・テーマの更新をメールで知らせてくれる |
| WAFチューニングサポート | 必要なときだけ | WAF の誤検知を除外する |
前回の教訓:SiteGuard の画像認証に加えて reCAPTCHA のプラグインも入れていたところ、そのプラグインが放置されていて検証が通らなくなり、正しいパスワードでもログインできなくなりました。詳しくは 管理画面に入れなくなった記事 にまとめています。
ログインURLを忘れたときの確認方法
「リダイレクトしない」を有効にすると、/wp-admin/ からログイン画面に飛べなくなります。URLを忘れたときは、次のどちらかで確認できます。
- ファイル管理で
public_html/.htaccessを開き、SiteGuard の記述の中にあるlogin_で始まる名前を見る - phpMyAdmin で次のSQLを実行し、
renamelogin_pathの値を見る
SELECT option_name, option_value FROM wp_options WHERE option_name LIKE 'siteguard%';
STEP 4. WordPress本体・テーマの設定
「メタ情報」ウィジェットを置かない
「ログイン」リンクに、変更後のログインURLがそのまま出ます。「外観」→「ウィジェット」から削除します。読者に必要なリンクはありません。
ユーザー登録を閉じる
「設定」→「一般」で、次のようにします。
- 「だれでもユーザー登録ができるようにする」:OFF
- 「新規ユーザーのデフォルト権限グループ」:購読者
ログインIDと表示名
- ログインIDに
adminなど推測されやすい名前を使わない - 「ユーザー」→「プロフィール」の「ブログ上の表示名」を、ログインIDとは別の名前にする
管理画面からのファイル編集を無効にする
乗っ取られたときに、管理画面からテーマやプラグインのファイルを書き換えられないようにします。wp-config.php の「編集が必要なのはここまでです」の行より上に追加します。
define( 'DISALLOW_FILE_EDIT', true );
更新と、プラグインの整理
- 「ダッシュボード」→「更新」で本体の自動更新の設定を確認し、プラグインとテーマは一覧の「自動更新を有効化」から設定する
- 使っていないプラグイン・テーマは、停止ではなく削除する
- WordPress.org の「最終更新」と「検証済み」のバージョンを見て、放置されたプラグインは入れない
検索結果ページを noindex にする
検索結果ページが検索エンジンに登録されると、SEOスパムに悪用されることがあります。STEP 1 のスクリプトで noindex が付いているか確認します(参考:WordPressの検索結果を悪用したSEOスパム攻撃)。
コメントスパム対策
Akismet などを使います(参考:WordPressはスパム対策だけで十分か)。
後回しでよいもの
readme.html:削除しても本体の更新で戻ってきます。今の WordPress はバージョンが載っていないので、後回しでかまいません- 外からの WP-Cron 実行:気になる場合は、
DISABLE_WP_CRONを設定し、サーバーの Cron から実行する方式に変えます
STEP 5. Jetpack を使うなら:XML-RPC と REST API は開けておく。その代わりに
Jetpack は、WordPress.com との通信に XML-RPC を使います。SiteGuard で XML-RPC を完全に止めたり、REST API をまとめて止めたりすると、Jetpack の統計などが動かなくなります。そこで、入口は開けたまま、別の方法で守ります。
| 開けておくもの | 何をされるか | その代わりの対策 |
|---|---|---|
| XML-RPC | パスワード総当たりの入口になる(このブログにも海外から来ていました) | SiteGuard の「ピングバック無効化」、Jetpack の「ブルートフォース攻撃からの保護」、エックスサーバーの国外IPアクセス制限(Jetpack は対象外)、長く推測されにくいパスワード |
REST API(/wp-json/wp/v2/users) | ユーザーのスラッグが見える | スラッグ(user_nicename)をログインIDと別の文字列にする |
XML-RPC 経由のログインにはログイン画面がないので、SiteGuard の画像認証は効きません。パスワードの強さと、総当たりへの対策が守りの中心になります。
スラッグをログインIDと別の文字列にする
WordPress のユーザーには、ログインIDとは別に、URLなどに使う「スラッグ(user_nicename)」があります。初期状態ではログインIDと同じ文字列なので、REST API・oEmbed・記事の著者リンクからログインIDが分かってしまいます。
phpMyAdmin で次のように変えると、この3か所がまとめて直ります。ログインIDは変わりません。
UPDATE wp_users SET user_nicename = '公開してよい名前' WHERE ID = 1;
- 著者ページのURLが
/author/公開してよい名前/に変わります(古いURLは404になります) - ログインIDとは必ず別の文字列にしてください
すでにログインIDが知られている場合
ログイン履歴に、実際のログインIDを使った総当たりが残っているなら、ログインIDそのものを変えることも考えます。
UPDATE wp_users SET user_login = '新しいログインID' WHERE ID = 1;
- 実行するとログアウトされます。実行する前に、ログインURLを控えておいてください
- 以降は、新しいログインIDかメールアドレスでログインします
Jetpack のブルートフォース保護
「Jetpack」→「設定」→「セキュリティ」の「ブルートフォース攻撃からの保護」が ON になっているか確認します(初期状態は ON)。自分が締め出されないように、よく使うIPアドレスを「常に許可するIPアドレス」に登録しておけます。
STEP 6. WPScan で外から確認する
設定が終わったら、WPScan で外から確認します。Docker で実行すれば、Mac に Ruby の環境を用意する必要はありません。
docker run --rm wpscanteam/wpscan:latest \
--url https://example.com/ \
--enumerate ap,cb,dbe,u1-10 \
--plugins-detection passive \
--plugins-version-detection mixed \
--throttle 300 \
--format cli-no-color --no-banner
| オプション | 意味 |
|---|---|
--enumerate ap,cb,dbe,u1-10 | プラグイン、設定ファイルのバックアップ、DBダンプ、ユーザー(ID 1〜10)を調べる |
--plugins-detection passive | ページ内の情報からだけプラグインを探す(総当たりしない) |
--throttle 300 | リクエストの間隔を 0.3 秒空ける |
- パスワードの総当たり(
--passwords)はやりません。 SiteGuard のログインロックで、自分のIPが締め出されるだけです - 強くスキャンしすぎると、自宅のIPがサーバーの WAF に弾かれることがあります
- ページに痕跡を残さないプラグイン(SiteGuard など)は、パッシブ検出では見つかりません。更新状況は管理画面で確認します
- 既知の脆弱性(CVE)と照合するには、WPScan で無料のAPIトークン(1日25回まで)を取得し、
--api-token "$WPSCAN_API_TOKEN"を付けて実行します
このブログで実行したときの結果は、次のとおりでした。
| 項目 | 結果 |
|---|---|
| 設定ファイルのバックアップ / DBダンプ | なし |
| WordPress 本体のバージョン | 外から特定できない |
| プラグイン | すべて最新 |
| ユーザー名 | REST API から取れた → STEP 5 で対策 |
| XML-RPC | 有効(Jetpack のために残す) |
外からの WP-Cron 実行 / readme.html | 有効 / 公開(優先度は低い) |
STEP 7. 月に1回の見直し
- STEP 1 のスクリプトを実行する
- SiteGuard のログイン履歴で、ログインに成功した記録に知らないIPがないか確認する
SELECT * FROM wp_siteguard_history WHERE operation = 0 ORDER BY id DESC LIMIT 10;
- APIトークンを付けて WPScan を実行する
- Googlebot や Google 検索から来た人にだけ別のページを見せる改ざんがないか確認する(手順は 管理画面に入れなくなった記事 にあります)
- プラグインの「最終更新」を見直し、使っていないものは削除する
まとめ:チェックリスト
| # | 場所 | やること | Jetpack を使う場合 |
|---|---|---|---|
| 1 | サーバー | WAF をすべて ON | 同じ |
| 2 | サーバー | 国外IPアクセス制限を ON | ON でよい(Jetpack は対象外) |
| 3 | サーバー | バックアップの保持期間を確認し、自前でも取る | 同じ |
| 4 | SiteGuard | ログインページ変更 +「リダイレクトしない」 | 同じ |
| 5 | SiteGuard | 画像認証・ログインロック・ログインアラート・詳細エラーメッセージの無効化 | 同じ |
| 6 | SiteGuard | XMLRPC防御 | ピングバック無効化にとどめる |
| 7 | SiteGuard | ユーザー名漏えい防御 | REST API 無効化は OFF |
| 8 | WordPress | 「メタ情報」ウィジェットを置かない | 同じ |
| 9 | WordPress | ユーザー登録を閉じる | 同じ |
| 10 | WordPress | スラッグをログインIDと別の文字列にする | 必須 |
| 11 | WordPress | DISALLOW_FILE_EDIT | 同じ |
| 12 | WordPress | 自動更新、使っていないプラグインの削除 | 同じ |
| 13 | Jetpack | ブルートフォース攻撃からの保護を ON | ― |
| 14 | 確認 | 外から確認するスクリプト、WPScan | 同じ |
| 15 | 運用 | 月に1回の見直し | 同じ |
セキュリティ対策は、入れて終わりではありません。外から見て、塞いで、また外から確認するの繰り返しです。プラグインを足すほど、障害の原因になりうる場所も増えます。サーバーの機能と SiteGuard を中心に、必要なものだけに絞るのがおすすめです。

コメント