
php2shell をきっかけに WordPress の nginx 周りを見直した
2026年10月21日前後に php2shell が物議を醸した。
本サイトも PHP + WordPress なので PHP については ASAP で 8.5.12 に上げたが、あわせて nginx 周りを見直したのでメモっておく。
仕事だとちゃんとやるが、プライベートなので nginx 周りは手抜きしていた。手抜きポイントは以下。
- レスポンスヘッダ
- Server ヘッダが nginx のバージョンを返していた
- X-Powered-By ヘッダで PHP のバージョンを返していた
- X-Vinyl ヘッダで Vinyl Cache を使っていることを返していた
- アップロードフォルダで PHP が PHP として動作していた
- xmlrpc.php や wp-login.php がアクセスし放題だった
今回はこの辺を見直した。
Table of Contents
レスポンスヘッダを制御
Server ヘッダや X-Powered-By ヘッダは nginx や PHP のバージョンを返してしまい、どの脆弱性が残っているかの重要なヒントとなる。Server ヘッダについては
server_tokens off;
でバージョンを削除できるが、X-Powered-By ヘッダはこれでは制御できない。
nginx + php-fpm というよくある構成の場合、 fastcgi_hide_header で FastCGI が受け取ったレスポンスヘッダのうち指定したヘッダを削除できる。今回はまずこれで対応した。
location ~ .*.php$ {
fastcgi_pass xxx.xxxx.xxx.xxx:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_hide_header X-Powered-By;
〜〜〜
}
万全を期す場合は ngx-headers-more を使うことになるが、nginx とは別モジュールなので別途インストールする。Gentoo の場合は標準の Portage tree に入っているので
emerge ngx-headers-more
すればよい。
インストール後は nginx.conf に以下を追記すれば Server ヘッダや X-Powered-By 、X-Vinyl ヘッダがそもそも送信されなくなる。
load_module modules/ngx_http_headers_more_filter_module.so;
http {
more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';
more_clear_headers 'X-Vinyl';
〜〜〜
}
アップロードフォルダで PHP を閉じる
WordPress の場合、デフォルトだと wp-content/uploads フォルダに任意のファイルをアップロードできる。WordPress 管理画面に侵入された場合は悪意のあるプラグインがインストールされることもあるが、アップロードフォルダ経由で悪意のある PHP がアップロードされることがあるので、仮にアップロードされたとしても PHP スクリプトとして実行できないようにしておく。
これはもうシンプルで、nginx.conf に以下の通り書いておけばいい。
server {
〜〜〜
location ^~ /wp-content/uploads/ {
try_files $uri =404;
location ~* \.(?:php\d*|phtml|phar|pht|phps)$ {
return 404;
}
}
location ~ .*.php$ {
fastcgi_pass xxx.xxxx.xxx.xxx:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_hide_header X-Powered-By;
〜〜〜
}
〜〜〜
}
location は上から適用されるので、PHP スクリプトファイルが FastCGI に投げられる設定の上で、アップロードフォルダ直下の PHP ファイルで 404 が返るように設定を書く必要がある。
xmlrpc.php や wp-login.php の呼び出し回数制限する
不正アクセスの端緒として、WordPress ログインに対するブルートフォース攻撃や xmlrpc.php にリクエストを飛ばすことが多い。よって、これにスロットリングをかけておくといい。
個人的には fail2ban の設定が好みではあるが、nginx 単体でも設定できる。
http {
〜〜〜
geo $xmlrpc_allowlist {
default 0;
127.0.0.1/32 1;
192.168.1.0/24 1;
# Jetpack の公開レンジをここに列挙しておく
122.248.245.244/32 1;
54.217.201.243/32 1;
54.232.116.4/32 1;
192.0.80.0/20 1;
192.0.96.0/20 1;
192.0.112.0/20 1;
195.234.108.0/22 1;
192.0.64.0/18 1;
}
map $xmlrpc_allowlist $xmlrpc_req_key {
0 $binary_remote_addr; # 制限対象: クライアント IP をキーに
1 "";
}
# 制限する閾値
limit_req_zone $xmlrpc_req_key zone=xmlrpc:60m rate=60r/m;
limit_conn_zone $xmlrpc_req_key zone=xmlrpc_conn:60m;
server {
〜〜〜
location = /xmlrpc.php {
limit_req zone=xmlrpc burst=10 nodelay;
limit_conn xmlrpc_conn 10;
limit_req_status 429;
limit_conn_status 429;
fastcgi_pass xxx.xxxx.xxx.xxx:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
〜〜〜
}
location ^~ /wp-content/uploads/ {
try_files $uri =404;
location ~* \.(?:php\d*|phtml|phar|pht|phps)$ {
return 404;
}
}
location ~ .*.php$ {
fastcgi_pass xxx.xxxx.xxx.xxx:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_hide_header X-Powered-By;
〜〜〜
}
}
}
Jetpack が xmlrpc.php を呼ぶので、Jetpack を使っている場合は適宜 Jetpack の IP アドレスを許可リストに記載しておく。
他にも WordPress 側で対応した方がいい内容もあるので、次の記事で書いておこうと思う。
PR
- nginx実践入門 WEB+DB PRESS plus
- マンガ+図解で基礎がよくわかる 情報セキュリティの教科書
- これからのWordPressセキュリティ本格講座: 「月1回・30分」のメンテナンスでサイトを安全に保つ!