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 がアクセスし放題だった

今回はこの辺を見直した。

レスポンスヘッダを制御

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