We sent 100,000 passwords at a WordPress site in under 40 minutes. Not at a client site. At ours.
Here is the part that should make you stop and reread that sentence: we did it in 402 HTTP requests. Not 100,000. Four hundred and two. Because XML-RPC lets you bundle multiple login attempts into a single HTTP request, and standard rate limiting counts each request once — not each attempt inside it. We set our bundle size to 500. That means Cloudflare, which was active on this site and actively proxying every request, saw roughly 29,000 HTTP requests instead of 14 million password attempts. Rate limiting bypassed by a factor of 500.
Nobody blocked us. No CAPTCHA. No 403. Two hundred OK responses per username, clean and fast, for three hours straight.
This kind of test has a name: a penetration test. What we did was run one against our own agency site — a WordPress and WooCommerce installation sitting behind Cloudflare — with the explicit goal of finding what a real attacker would find before a real attacker found it. The results were instructive enough that we decided to write them up.
The short version: Cloudflare's presence did not protect the site from a single one of the attacks we ran. User enumeration, XML-RPC bruteforce, WPScan active enumeration, version fingerprinting, wp-cron exposure — all of it went through unimpeded. This is not a criticism of Cloudflare specifically. It is a statement about what a CDN and WAF are actually designed to stop, versus what people assume they stop.
Cloudflare is designed to absorb volumetric attacks. DDoS mitigation. Known bad IP ranges. Bot signatures at scale. It is very good at those things. What it does not understand — and what no generic firewall rule understands by default — is the WordPress application layer. It does not know that a single POST to /xmlrpc.php with 500 system.multicall entries represents 500 login attempts, not one. It does not know that a GET to /wp-json/wp/v2/users hands an unauthenticated visitor a list of admin usernames. Those are WordPress-specific attack surfaces, and they require WordPress-level controls.
Let's walk through what we actually found, because the individual findings matter less than the pattern they form.
First, usernames. Before any password attempt, we extracted two confirmed admin-level usernames using two completely separate methods, neither of which required any authentication. The WordPress REST API returns a full user listing by default — including names, slugs, and IDs — unless you explicitly disable that endpoint. The author redirect does the same thing in a different way: request /?author=1 and the server redirects you to /author/admin/. Request /?author=2 and it tells you the second admin account. Two requests, two confirmed usernames, no credentials required.
This matters because a username is half the credential pair. If you know who to attack, you have already done a significant portion of an attacker's reconnaissance work for them.
Second, registration. The site had open registration enabled. Any visitor could create an account. The site's SMTP was not configured, so the confirmation email that was supposed to go out never did. The result: anyone could create a subscriber account, confirm nothing, and log in. This is not an edge case. It is the default WordPress behavior when someone enables WooCommerce and forgets to configure email.
Every plugin you install is a software dependency. Every dependency has a vulnerability history. Every vulnerability has a disclosure date, and between that date and the date you update, your site is exposed to anyone who reads the same database you are not reading. The plugin ecosystem for WordPress is enormous. The quality varies wildly. The maintenance cadence varies more. Some plugins have not seen a commit in three years. Some have tens of millions of installs and serious CVEs that took months to patch.
The only plugin that cannot be exploited is the one you did not install.
This does not mean you should build everything yourself from nothing. It means you should audit what you have and be honest about what each plugin is actually doing. A surprising number of WordPress sites are running plugins for tasks that are genuinely simple to implement in a few lines of PHP. A contact form that emails you. A redirect rule. A cron job replacement. A small piece of login hardening. For each of those, there is an alternative: write the code yourself, own it, and eliminate the dependency.
The XML-RPC fix is the clearest example we have from our own audit. One line:
add_filter('xmlrpc_enabled', '__return_false');
One line in wp-config.php eliminates the entire XML-RPC attack surface. No plugin needed. No dependency. No additional code path someone else wrote and maintains on their own schedule.
The broader principle here is attack surface minimization. Every exposed endpoint is something that needs to be defended. Every plugin is a dependency that needs to be updated. Every open registration form is an account that can be created. Every piece of version information disclosed is a map an attacker can use to look up your CVEs.
You cannot defend what you have not audited. And you cannot audit what you have not mapped.
The question to ask about every part of your WordPress installation is not does this work but does this need to be accessible. Does XML-RPC need to be on? Does the REST API users endpoint need to be public? Does readme.html need to exist? Does wp-cron.php need to be hittable from the open internet? For each of these, the answer for most sites is no. And for each no, the fix is often one line of code or one server-side block.
What we found when we ran through that checklist on our own site: five of six surface findings, fixed in a single session. Not with plugins. With small targeted code changes and server-side configuration.
The threat landscape for WordPress is not getting simpler. There are currently over 100,000 plugins in the WordPress repository. CVE disclosures for WordPress plugins have been trending upward year over year. WPVulnDB and Patchstack track new findings daily. The attack tooling — WPScan — is free and takes three minutes to install.
The attackers running these tools are not all sophisticated. Most of them are running automated scripts against bulk target lists. They are not targeting you specifically. They are scanning everything they can reach and seeing what responds to known techniques. XML-RPC enumeration is in every WordPress scan tool. User enumeration is in every WordPress scan tool. Version fingerprinting is in every WordPress scan tool.
The question is not whether someone will run these scans at your site. They already are. The question is what they find when they do.
If they find an XML-RPC endpoint accepting 500 bundled login attempts per request, no username protection, open registration, and a version disclosure that maps directly to NVD entries — they have everything they need to build an attack chain.
If they find a 403, a 404, and nothing useful; they move on.
That is what attack surface minimization actually looks like in practice. Not perfect security. Not an impenetrable wall. Just fewer things to grab onto. Just a harder target than the next one.
The site we audited is now significantly closer to that. Most of its surface findings took under twenty minutes to address. The gap was not technical complexity. It was not knowing what to look for until we went looking.
That is what this kind of audit is for.
