~/blog/wordpress-rest-api-user-enumerationPOSTED
by Shady Nathan Tawfik · May 9, 2026 · 5 min

WordPress User Enumeration Is Not The Finding, It Is The Multiplier

A tax advisory firm exposed every account slug through its public REST API, shipped no security headers at all, and left the login form without rate limiting.

Interactive[blog-2.0]

TL;DR: The REST API of a WordPress estate listed every account slug without authentication, and the login had no rate limit. I demonstrate the enumeration, show why the login error text makes it worse, and how I hardened the endpoint.

The scenario

I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.

A medium-sized tax advisory firm running a stock WordPress installation with an active theme and a broad plugin set. The engagement covered enumeration paths, login abuse resistance, header posture, mail authentication and file exposure.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

The pattern I recognised — and that I keep finding in similar estates:

Enumeration is usually filed as a low severity informational finding because, on its own, it discloses nothing but a username. That framing is wrong. A username is not the vulnerability; it is the input that removes the first obstacle in a credential attack. Without it, an attacker sprays a million addresses against an unknown user population. With it, the same effort targets a handful of known-valid accounts.

The WordPress REST API makes this cheap by design. A single unauthenticated GET against the users collection returns a paginated list of accounts with slugs, display names, avatars and, on newer core versions, role metadata. There is no rate limit, no authentication and no lockout involved. The same information is also reachable through the login form error messages and through author query redirects, so a defender who blocks one route frequently overlooks the other two.

The multiplier is what happens next. Enumeration plus an unthrottled login form equals credential stuffing, because attackers already hold breach corpora keyed on the same addresses. Enumeration plus no security headers means the follow-up requests carry no browser fingerprint at all. Enumeration plus a disclosed mail authentication posture means the resulting phish is trivial to send from the real domain.

TL;DR: A tax advisory firm exposed every account slug through its public REST API, shipped no security headers at all, and left the login form without rate limiting.

Findings

SeverityFindingEvidence
HIGHAccount enumeration through the public REST APINine accounts with slugs, names and avatar hashes
HIGHNo security headers site-wideNo transport policy, no frame policy, no content type policy
HIGHLogin form without rate limiting or lockoutUnlimited attempts, no progressive delay
MEDIUMPlugin and core versions disclosedGenerator meta and asset query strings
MEDIUMMail policy set to neutralAnyone may send as the domain
LOWBackup and debug artefacts predictableCommon paths returned distinguishable responses

The attack path

Breach corpus of email and password pairsRestrict to discovered usernamesStuff the login form with no lockoutSession cookie issuedEnumerate admin-capable accountsPassword reset abuse

Reproduction in my lab

Every command below targets a lab container I control. Nothing here is aimed at a live system.

Read the public user collection from a lab instance

~/code/bash bash
# target: a lab WordPress you control
$ curl -s http://localhost:8080/wp-json/wp/v2/users?per_page=100 \
  | python3 -c 'import json,sys; [print(u["id"], u["slug"]) for u in json.load(sys.stdin)]'

# the author redirect route leaks the same information
$ curl -so /dev/null -w '%{http_code} %{redirect_url}\n' 'http://localhost:8080/?author=1'

# and the login form distinguishes a bad user from a bad password
$ curl -s -d 'log=nosuchuser&pwd=x' http://localhost:8080/wp-login.php | grep -io 'invalid username\|unknown user'

Detection in your own estate

Enumerate and check headers in one pass

~/code/bash bash
# read-only header probe against a host you own or are authorised to test
$ curl -sI https://example.org/ | grep -iE \
  'strict-transport|content-security-policy|x-frame-options|x-content-type|referrer-policy|permissions-policy'

# presence check, exit code 0 when every header is present
$ for h in strict-transport-security content-security-policy x-frame-options \
           x-content-type-options referrer-policy; do
    curl -sI https://example.org/ | grep -qi "^$h" && echo "ok   $h" || echo "MISS $h"
  done

Check the mail posture that makes follow-up phishing cheap

~/code/bash bash
# SPF, DKIM and DMARC for a domain you own
$ dig +short TXT example.org | grep -i spf
$ dig +short TXT _dmarc.example.org

# a soft fail (-all is the goal, ~all means anyone can forge)
$ dig +short TXT example.org | grep -oE '[-+~?]all'

Remediation

Deny anonymous access to the user collection at the edge

~/code/nginx nginx
# block unauthenticated user enumeration, keep the rest of the API open
location ~* ^/wp-json/wp/v2/users {
    if ($remote_user = "") { return 403; }
    proxy_pass http://backend;
}

# and do not answer the author redirect at all
location ~* ^/\?author=[0-9]+ { return 404; }

Neutralise the login error difference and throttle attempts

~/code/ini ini
# wp-config.php: do not reveal whether the account exists
define( 'LOGGED_IN_KEY', 'generate-with-openssl-rand-base64-48' );

# fail2ban style rule, applied by the host, not by the application
[Definition]
failregex = ^<HOST> .*POST /wp-login\.php.* 401
maxretry = 5
findtime = 600
bantime = 3600

Once an attacker gains access, they can perform the following actions:

Account enumeration through the public REST API
No security headers site-wide
Login form without rate limiting or lockout
Plugin and core versions disclosed
Mail policy set to neutral

Takeaways

  • Rank enumeration by what it enables, not by what it returns.
  • Block all three enumeration routes: REST collection, login error text and author redirect.
  • Enumeration is only half the problem, rate limiting on the login form is the other half.
  • Mail authentication failures turn any successful login into a convincing phish.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026