Skip to content

SECURITY Bug: xmlrpc.php deny rule in WordPress nginx config is unreachable due to location ordering #279

Description

@ajoe

Found by Claude.AI Opus 4.7:

Bug: xmlrpc.php deny rule in WordPress nginx config is unreachable due to location ordering

Summary

The WordPress app's nginx configuration contains a location block intended to deny public access to xmlrpc.php, but the rule never fires. Requests to /xmlrpc.php reach PHP-FPM and are handled by WordPress (which responds with 405 Method Not Allowed for GET, and would happily process POST), defeating the intended hardening.

Affected file

/etc/nginx/conf.d/<domain>.d/wordpress.conf (installed by the WordPress YunoHost package)

Reproduction

  1. Install the WordPress app on a YunoHost instance.
  2. Request https://<domain>/xmlrpc.php:
    curl -i https://<domain>/xmlrpc.php
    
  3. Expected: 403 Forbidden (per the # Deny public access to xmlrpc.php rule).
  4. Actual: 405 Method Not Allowed (response from WordPress, i.e. the request was passed to PHP-FPM).

Root cause

In wordpress.conf, inside location / { ... }, the generic PHP handler is declared before the xmlrpc deny rule:

location / {
    ...
    location ~ [^/]\.php(/|$) {            # ← matches xmlrpc.php first
        fastcgi_pass unix:/var/run/php/phpX.Y-fpm-wordpress.sock;
        include fastcgi_params_no_auth;
    }

    ...

    location ~* xmlrpc\.php {              # ← never reached
        deny all;
        return 403;
    }
}

For regex (~ / ~*) location blocks, nginx uses the first match in the order they are declared, not the most specific one. Since [^/]\.php(/|$) matches xmlrpc.php, the deny block is unreachable.

The same bug exists in the wordpress.conf snippets shipped for related WordPress variants (e.g. wordpress__2.conf, wordpress__3.conf), where the rule additionally uses an unescaped dot (xmlrpc.php instead of xmlrpc\.php) — minor, but worth fixing while there.

Suggested fix

Move the xmlrpc deny block (and similarly wp-config.php) above the generic PHP handler, and escape the dot:

location / {
    ...
    client_max_body_size 512M;

    # Deny public access to xmlrpc.php
    location ~* xmlrpc\.php {
        deny all;
        return 403;
    }

    # Deny public access to wp-config.php
    location ~* wp-config\.php {
        deny all;
        return 403;
    }

    location ~ [^/]\.php(/|$) {
        fastcgi_pass unix:/var/run/php/phpX.Y-fpm-wordpress.sock;
        include fastcgi_params_no_auth;
    }
    ...
}

After reload (nginx -t && systemctl reload nginx), curl -i https://<domain>/xmlrpc.php correctly returns 403.

Impact

  • Hardening that operators (and the YunoHost docs) believe is in place is silently ineffective.
  • xmlrpc.php remains reachable for brute-force / pingback amplification attacks against every WordPress instance installed via the package.

Environment

  • YunoHost WordPress app (current Debian-packaged version)
  • nginx (any version — this is a config-ordering issue, not version-specific)
  • Reproduced on production instance running PHP 8.5-FPM

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions