WordPress REST API Security: What to Lock Down
Diagnosis

WordPress REST API Security: What to Lock Down

Most WordPress security guides tell you to disable the REST (Representational State Transfer) API. That advice breaks Gutenberg, WooCommerce, and half your plugins. The real problem is narrower: one endpoint, `/wp-json/wp/v2/users`, hands attackers a username list in a single HTTP request. Here is what actually leaks, what to restrict, and what to leave alone.

What the REST API actually exposes

The `/wp-json/wp/v2/users` endpoint does not leak passwords. It does not leak email addresses. It does not leak roles, capabilities, registered dates, or meta fields. Those require an authenticated request with the `edit` context. Anyone telling you the endpoint hands attackers your admin credentials is selling fear.

What it does return at anonymous level: `id`, `name`, `url`, `description`, `link`, `slug`, and `avatar_urls` for any user who has published a post or page. The official WordPress REST API Handbook documents this behaviour explicitly.

The field that matters is `slug`. The slug is the `user_nicename`, a URL-friendly sanitised version of the login name. On a default WordPress install, the nicename is identical to `user_login`. A user who registers as `john` has nicename `john`. A user who registers as `john.doe` may have nicename `john-doe`. In most cases the slug is a valid login identifier.

That is the whole problem in one sentence: the endpoint hands out a list of likely usernames for every published-content author, in a single anonymous HTTP request, in a clean JSON array.

Other endpoints reachable without authentication include `/wp-json/wp/v2/posts`, `/wp-json/wp/v2/pages`, `/wp-json/wp/v2/media`, `/wp-json/wp/v2/comments` (approved only), `/wp-json/wp/v2/categories`, and the `/wp-json/` root listing the available namespaces. None of these are the issue. They serve published content that is already public on the site. The issue is one endpoint and the user-enumeration vector it creates.

How attackers use it: the enumeration flow

The threat is not someone manually visiting your site. It is automated bulk scanning at machine speed.

Here is the four-step flow. First, a scanner (WPScan, a Nuclei template, or a one-line `curl`) requests `/wp-json/wp/v2/users` against your site. Second, the response is a paginated JSON array — the scanner iterates pages and extracts every `slug` value. Third, each slug is treated as a candidate username; where `user_nicename` matches `user_login` (the default), every slug is a valid login. Fourth, the slug list is fed into a credential-stuffing tool against `/wp-login.php` or `/xmlrpc.php`, paired with passwords from public breach databases.

A single request demonstrates the harvest: `curl -s https://example.com/wp-json/wp/v2/users | jq '.[].slug'`. That returns the slug array. No authentication, no rate limit unless you put one there, no log entry that distinguishes it from a legitimate API client.

CVE-2017-5487 was assigned in January 2017 against WordPress 4.7.0, which returned all users regardless of whether they had published content. WordPress 4.7.1 narrowed the endpoint to published-content authors only. Mozilla's security team rejected a 2020 bug-bounty report on the current behaviour with the verdict "This list of user names is all considered to be public data". That framing is correct but incomplete.

Wordfence's 2024 annual report tallied over 55 billion password attacks across its network for the year. Credential-stuffing campaigns are the dominant brute-force pattern, and they need username lists to run. The endpoint is "public data" the way an unlocked filing cabinet in a public lobby is public. Technically anyone could open it. That does not mean you leave it unlocked.

Why disabling the REST API is the wrong move

The standard security-listicle advice is: install a plugin called something like "Disable WP REST API" and turn the whole thing off.

Don't.

The official WordPress REST API FAQ states it directly: "You should not disable the REST API; doing so will break WordPress Admin functionality that depends on the API being active".

What breaks when you globally disable the REST API: Gutenberg block editor loads blank or non-functional — it depends on REST API calls to load block data, save content, and manage media (Gutenberg issue #8549 has tracked this since 2018). WooCommerce's mobile app, checkout block, and several admin screens use REST API endpoints. Contact Form 7, Yoast SEO admin screens, Elementor block library, and Jetpack all make REST API calls from `wp-admin`. Headless and decoupled frontends are entirely dependent on the REST API by definition.

The attack surface you are worried about is one endpoint, `/wp/v2/users`, plus the `?author=N` redirect. Removing the entire API to fix a one-endpoint problem is the wrong tool. The fix is surgical, not blunt.

What to actually lock down

Two server-side controls handle the bulk of automated enumeration: a filter that locks the REST users endpoint, and a rewrite rule that kills the `?author=N` redirect. Both are needed. Locking only one leaves the other vector wide open.

Restrict `/wp/v2/users` to authenticated requests. The `rest_pre_dispatch` filter returns a 401 for unauthenticated callers hitting the users endpoint while keeping it available for authenticated admin sessions, so Gutenberg's user-mention blocks and similar features keep working. Drop this into a snippet plugin (WPCode or Code Snippets work well) or your theme's `functions.php`:

add_filter('rest_pre_dispatch', function($result, \WP_REST_Server $srv, \WP_REST_Request $request) {
    $method = $request->get_method();
    $path   = $request->get_route();
    if (('GET' === $method || 'HEAD' === $method) && preg_match('!^/wp/v2/users(?:$|/)!i', $path)) {
        if (!current_user_can('list_users')) {
            return new \WP_Error(
                'rest_user_cannot_view',
                'Sorry, you are not allowed to use this API.',
                ['status' => rest_authorization_required_code()]
            );
        }
    }
    return $result;
}, 10, 3);
php

Source: Wild Wolf's REST API restriction guide. Test it by hitting `/wp-json/wp/v2/users` while logged out (you should get a 401 with `rest_user_cannot_view`) and again while logged in as admin (the full user list should still return). If you do not need the endpoint at all, the `rest_endpoints` removal is an option, but it blocks authenticated callers too. Use `rest_pre_dispatch` unless you have a specific reason not to.

If you do remove the endpoint entirely instead:

add_filter('rest_endpoints', function($endpoints) {
    if (isset($endpoints['/wp/v2/users'])) {
        unset($endpoints['/wp/v2/users']);
    }
    if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) {
        unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    }
    return $endpoints;
});
php

Block `?author=N` redirects. A locked REST endpoint protects nothing if `https://example.com/?author=1` still redirects to `https://example.com/author/john/` and reveals the nicename. Scanners iterate IDs from 1 upward and harvest the same data. The `RewriteCond` excluding `/wp-admin/` is required — without it you break author filtering inside the admin.

Apache — add to `.htaccess` before `# BEGIN WordPress`:

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/wp-admin/ [NC]
RewriteCond %{QUERY_STRING} author=([0-9]*) [NC]
RewriteRule .* - [F,L]
apache

Nginx — add inside the server block:

if ($query_string ~* "author=([0-9]*)") {
    return 403;
}
nginx

Source: Perishable Press's user-enumeration writeup. Verify by visiting `https://example.com/?author=1` — you should get a 403, not a redirect.

Rate-limit /wp-json at the edge

If Cloudflare is already in your stack, add a WAF (Web Application Firewall) rate-limiting rule. This catches scanner bursts before they touch your origin and complements the PHP filter rather than replacing it.

Cloudflare dashboard path: Security > WAF > Rate limiting rules. Threshold: 30 requests per 10 seconds per IP. Action: Managed Challenge. Rule expression:

http.request.uri.path contains "/wp-json/"
cloudflare

Source: DCHost's Cloudflare WAF playbook.

Legitimate plugin traffic from a single client almost never crosses 30 requests in 10 seconds against `/wp-json/`. Scanner bursts routinely do. The Managed Challenge action lets a real user or a misconfigured legitimate client clear the challenge and continue.

If you do not have Cloudflare, the same principle applies at any reverse proxy or WAF in front of your site. The point is to add friction at the edge before requests reach PHP. Run a free scan to verify the lockdown landed — the scanner flags both the `/wp-json/wp/v2/users` exposure and the `?author=1` redirect.

Edge cases: headless WordPress and plugin-introduced routes

Headless setups (decoupled frontend consuming WordPress through REST or WP-GraphQL) cannot follow the "restrict all unauthenticated reads" approach. Anonymous visitors need to read posts and pages by definition.

For headless: keep `/wp/v2/posts`, `/wp/v2/pages`, and `/wp/v2/categories` open. Still restrict `/wp/v2/users`. Your frontend does not need it for public display. For service-to-service authentication on write operations, use Application Passwords (built into WordPress since 5.6) over HTTPS. For SPA and mobile clients, JWT (JSON Web Token) authentication via a vetted plugin is the standard option per the REST API authentication docs.

Plugin-introduced REST routes are a separate threat surface. Patchstack's H1 2025 vulnerability report shows 57% of WordPress vulnerabilities required no authentication to exploit, and 89% lived in plugins, not core. The `/wp/v2/users` lockdown does nothing for any of those. They need the WAF rule and they need plugins kept current.

A cautionary note: WP Cerber Security, a plugin that explicitly claims REST API protection, shipped a user-enumeration bypass via REST API in versions up to and including 9.3.2 (fixed in 9.3.3). The lesson is not that WP Cerber is bad. It is that any tool can have a bypass, and defense-in-depth matters.

What security plugins actually do here

No free plugin does all of this for you. Know what each tool actually covers, and keep core current alongside whatever plugin you pick.

Wordfence (free + premium): the WAF blocks exploitation of known plugin REST API CVEs. It does not restrict `/wp/v2/users` by default at the free tier. The value is known-CVE blocking, not enumeration hiding. Wordfence flagged the username-harvesting risk publicly back in December 2016 when the REST API first shipped in core, but the default rule set has not historically locked the endpoint out of the box.

Solid Security (formerly iThemes Security) has a "REST API Restrict Access" toggle under WordPress Tweaks > Advanced Settings. When enabled, most requests require a logged-in user. This is the `rest_authentication_errors` approach wrapped in a UI toggle — verify which tier the toggle ships in for your version before relying on it. Patchstack (free community + premium): monitors and virtually patches plugin-introduced vulnerabilities. It does not touch the core users endpoint. Snippet plugins (WPCode, Code Snippets) are the practical delivery mechanism for the `rest_pre_dispatch` snippet — paste, activate, deactivate if anything breaks.

Core REST API security is also actively maintained. Running an outdated WordPress is the easiest way to undo every snippet above. WordPress 6.8.3 (released 30 September 2025) patched `class-wp-rest-users-controller.php` directly, alongside the posts and terms controllers — it addressed "a data exposure issue where authenticated users could access some restricted content". Late-2025 releases (6.9.x) also patched a "Missing Authorization to Authenticated (Subscriber+) Arbitrary Note Creation via REST API" issue at CVSS 4.3. WordPress 6.8 itself moved password hashing to bcrypt and switched application passwords to BLAKE2b via Sodium, which makes credential-stuffing against breached hash databases meaningfully harder when usernames have been harvested.

For free security plugins in general, the rule of thumb is the same: each tool covers one slice. Stack them deliberately, and do not assume any single one covers REST API hardening end to end.

Related fix guides

GuardingWP checks your site for the 11 most common WordPress vulnerabilities — plus scans your installed plugins against the known CVE database. Free, no account required.

Check if your site exposes wp-json/wp/v2/users →