Christmas offer: sign up before 31 December and get the Starter plan free for 2 months.

Predax Blog

Product updates, best practices, and deep dives into IP intelligence.

Stop WordPress Brute-Force Attacks: Login Floods, User Enumeration, and XML-RPC Amplification in 2026
โ€ขPredax Team

Stop WordPress Brute-Force Attacks: Login Floods, User Enumeration, and XML-RPC Amplification in 2026

wordpressbrute-forcexml-rpcuser-enumerationwafwordpress-security

Automated brute-force against wp-login.php gets all the attention, but it's usually the least efficient part of the attack. Before a single password guess lands, most WordPress compromises start with reconnaissance: a script quietly working through ?author=1, ?author=2, and so on, harvesting valid usernames from redirects. Or a single POST to xmlrpc.php that bundles a thousand login attempts inside one HTTP request, invisible to anything watching request counts. By the time a human would call it an "attack," the automation already knows which usernames are real and has tried more passwords against them than your rate limiter ever saw.

TL;DR

  • Brute-force, user enumeration, and XML-RPC amplification are three distinct techniques โ€” enumeration and amplification produce little or no signal in a normal failed-login log.
  • Strong passwords and even 2FA protect the login itself, but they don't stop the reconnaissance and volume-hiding tricks that precede it.
  • The fix has to happen at the IP/request layer, before WordPress's own auth, REST, or XML-RPC code ever evaluates the request.
  • Predax Security ships three purpose-built defenses for this exact problem: user/author enumeration blocking, XML-RPC hardening, and WAF auto-ban escalation โ€” all live today.

Attack anatomy

Credential-stuffing floods against wp-login.php. Attackers don't guess passwords โ€” they replay huge lists of already-leaked username/password pairs (from unrelated breaches) against your login form, banking on password reuse. Volume is the whole strategy: try enough pairs across enough sites and a percentage will work purely on reuse odds. For the mechanics of this at the credential-list level, see our complete guide to credential-stuffing attacks.

User/author enumeration. WordPress's default author archive behavior means requesting ?author=1 redirects to /author/admin/ (or whatever the real username is) โ€” and it does that for every author ID whether or not you're logged in. Walk the IDs from 1 upward and you've harvested every valid username on the site without a single failed login attempt. The WP REST API's /wp/v2/users endpoint does the same job in one request, returning a full list of registered usernames to anyone who asks. Neither of these trips a login-failure counter, because neither one is a login attempt โ€” it's reconnaissance, done quietly, so the credential-stuffing pass that follows can skip the guesswork of "which usernames even exist" and go straight to trying passwords against ones it knows are real.

XML-RPC `system.multicall` amplification. xmlrpc.php exists so WordPress mobile apps and services like Jetpack can talk to your site over a single API, and one of its features lets a caller bundle many method calls into one HTTP request via system.multicall. That's convenient for legitimate integrations โ€” and a gift to attackers, who can pack thousands of wp.getUsersBlogs login-attempt calls into a handful of POST requests. Your server processes them all in one connection. A per-request rate limiter counting "requests per minute" sees a trickle; what's actually happening is a flood, just hidden inside the request bodies instead of the request count.

Server rack cabling under dim green lighting, representing the tangled, high-volume traffic automated brute-force and amplification tools generate against a WordPress login
Server rack cabling under dim green lighting, representing the tangled, high-volume traffic automated brute-force and amplification tools generate against a WordPress login

Why passwords and 2FA alone aren't enough

None of this is really about how strong your password is. Credential-stuffing lists are distributed across large IP pools and rotate constantly, so per-account lockouts and even 2FA only raise the cost of the *final* guess โ€” they do nothing to the scanning and enumeration that precede it, and that reconnaissance phase is often disguised behind rotating residential and datacenter proxy infrastructure specifically so it doesn't cluster on a handful of obviously-suspicious IPs.

Enumeration attacks are the clearest example of why account-level defenses miss the point entirely: there's no login attempt to rate-limit. A visitor requesting ?author=7 a hundred times in a row isn't triggering a failed-password counter โ€” they're not attempting a password at all. Any defense built around "count the failed logins" has literally nothing to count.

XML-RPC amplification breaks the same assumption from a different angle. A per-request threshold ("block after 20 requests/minute") looks at request *count*, and system.multicall keeps the count low while hiding the volume inside the payload. The attacker isn't sending more requests โ€” they're sending fewer, bigger ones.

The IP-signal layer

The common thread across all three techniques is that they need to be stopped before WordPress's own login, REST, or XML-RPC handling code ever runs โ€” every request that reaches that code has already cost your server a database connection, a plugin/theme bootstrap, and CPU cycles, whether or not it ultimately gets denied. Once a request is in-flight, you're already paying for it.

IP-layer signals โ€” reputation, request velocity, datacenter and proxy origin โ€” identify this traffic as automated before a single credential is checked or a single author ID is walked. A visitor coming from a known datacenter range hitting ?author=N in sequence, or a burst of XML-RPC POSTs from an IP with no prior legitimate history, looks nothing like a real visitor's traffic pattern, and that difference is visible at the network layer regardless of what the request body contains. For the broader case on why VPN/proxy/datacenter signals matter for a WordPress site generally, see blocking VPNs, proxies, and Tor on WordPress.

What Predax Security actually ships

Predax Security already blocks brute-force at the account level via configurable login-threshold blocking โ€” lock out an IP after N failed attempts in a window (see setting login thresholds for the specifics). A July 2026 release added three more targeted defenses that go after the reconnaissance and amplification techniques login-threshold blocking was never designed to catch.

User/author enumeration blocking (ipsentry_block_user_enumeration, off by default โ€” enable it in Settings). When switched on, it blocks ?author=<N> probes for logged-out front-end visitors โ€” logged-in users, AJAX calls, and REST requests are deliberately left untouched so it doesn't interfere with normal site operation. It returns HTTP 401 on the WP REST /wp/v2/users and /wp/v2/users/<id> endpoints for unauthenticated requesters, with the message "Listing users is not permitted," and it strips author identity fields out of oEmbed responses too. Every blocked attempt is logged with reason user_enumeration, shown in the Threat Log as USER_ENUM.

XML-RPC hardening (ipsentry_xmlrpc_hardening) gives you three states โ€” Off, "Block amplification," and "Disable all." The recommended middle setting is labeled in the plugin UI as "Block amplification โ€” pingback + system.multicall (recommended)," with the description: "'Block amplification' stops brute-force amplification and pingback DDoS while keeping normal XML-RPC (e.g. the WordPress mobile app) working." Both hardened modes block system.multicall by inspecting the raw request body directly โ€” necessary because system.multicall is registered on WordPress's XML-RPC callback list *after* the standard xmlrpc_methods filter runs, so the usual filter-based approach can't remove it. "Block amplification" additionally removes just the two pingback methods (pingback.ping and pingback.extensions.getPingbacks) โ€” the SSRF/DDoS-reflection vector โ€” while leaving other XML-RPC methods, like the ones the WordPress and Jetpack mobile apps rely on, working normally. "Disable all" turns XML-RPC off entirely. Both hardened modes also strip the X-Pingback HTTP header.

WAF auto-ban escalation (ipsentry_waf_escalation_enabled, off by default, with ipsentry_waf_escalation_threshold defaulting to 5 and a minimum of 2). When a visitor trips the plugin's WAF a configurable number of times inside a rolling 10-minute window, they get a durable temporary ban โ€” 30 minutes by default, configurable โ€” applied across *every* surface the plugin protects, not just the one endpoint that set it off. So a visitor who trips the WAF on a login-related probe gets shut out of XML-RPC, the REST API, and everywhere else the plugin watches, for the ban duration. It's logged with reason waf_escalation, shown as WAF_BAN, and it skips WP-CLI/cron requests, whitelisted IPs, and logged-in administrators so it can't lock you out of your own maintenance tooling.

All three live under Settings โ†’ WordPress Hardening in the plugin, alongside the existing login-threshold controls. Full setup details are on the WordPress integration page.

A well-organized, actively-cabled server rack, representing the layered set of purpose-built defenses Predax Security's hardening pack adds to a WordPress install
A well-organized, actively-cabled server rack, representing the layered set of purpose-built defenses Predax Security's hardening pack adds to a WordPress install

Self-check + code example

Three settings, thirty seconds:

  1. Is "Block user / author enumeration" checked?
  2. Is XML-RPC protection set to "Block amplification" or "Disable all" โ€” not "Off"?
  3. Is WAF auto-ban escalation enabled?

If any of those are off, the corresponding attack path above is open right now regardless of how strong your admin password is.

If you're gating your own login flow (a custom login form, a headless WP setup, or an app talking to wp-login.php programmatically) and want to check an IP's risk before evaluating credentials at all, a Predax lookup on the login hook looks like this:

<?php
/**
 * Gate a login attempt on IP risk before WordPress evaluates credentials.
 * Hook this into 'authenticate' (early priority) or your own login handler.
 */
function predax_check_login_ip( $ip ) {
    $cache_key = 'predax_ip_' . md5( $ip );
    $cached    = get_transient( $cache_key );
    if ( false !== $cached ) {
        return $cached;
    }

    // POST with a JSON body โ€” the endpoint has no GET variant; a GET 405s.
    $response = wp_remote_post(
        'https://predax.io/api/v1/check/ip',
        array(
            'headers' => array(
                'X-API-Key'    => PREDAX_API_KEY,
                'Content-Type' => 'application/json',
            ),
            'body'    => wp_json_encode( array( 'ip' => $ip ) ),
            'timeout' => 3,
        )
    );

    // Fail open: if the API call itself fails, don't block a real login attempt.
    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        return null;
    }

    $data = json_decode( wp_remote_retrieve_body( $response ), true );
    set_transient( $cache_key, $data, HOUR_IN_SECONDS );

    return $data;
}

add_filter( 'authenticate', function ( $user, $username, $password ) {
    if ( empty( $username ) || empty( $password ) ) {
        return $user; // let WP handle its own empty-field errors
    }

    $ip   = $_SERVER['REMOTE_ADDR'] ?? '';
    $risk = predax_check_login_ip( $ip );

    // risk_score lives under 'classification' โ€” a top-level read returns
    // null silently and the gate never fires. (We shipped that exact bug
    // three times in our own plugins.)
    if ( $risk && ( $risk['classification']['risk_score'] ?? 0 ) >= 85 ) {
        return new WP_Error(
            'predax_blocked',
            __( 'Login temporarily unavailable from this network.', 'predax' )
        );
    }

    return $user;
}, 5, 3 );

This runs before WordPress checks the password, so a high-risk IP is turned away without your site ever hashing a credential comparison for it โ€” the same fail-open, hourly-cache pattern used elsewhere on the blog, just wired to the login gate instead of checkout.