Tech11 min read

WordPress REST API security: Application Passwords, CORS, rate limits in 2026

IkesanContents

TL;DR

Who’s affected Sites that use the WordPress REST API for external integrations: headless frontends, CI scripts, AI tools, MCP Adapter

What to do

  1. Issue one Application Password per integration, tied to a dedicated low-privilege user
  2. Stop treating CORS as authorization — every write route needs a nonce, credentials, or a capability check
  3. Put rate limiting in front of PHP (nginx limit_req, WAF, hosting layer), not inside WordPress

Real-world CVEs Post SMTP’s CVE-2025-24000 (permission_callback that only checked login state), Simple JWT Login’s privilege escalation CVE-2026-14262


If a site uses the WordPress REST API for external integrations, /wp-json/ has to be split into two different things: a public read API and a privileged write API.
The paths into WordPress from outside keep multiplying — headless frontends, AI integrations, MCP Adapter, the Connectors API — and once the two roles are mixed, a leaked integration credential becomes control over the site itself.

WP Admin Lab published a post on WordPress REST API authentication, rate limiting, and CORS on July 14.
It is not about a new individual CVE; it re-tightens Application Passwords, JWT, rate limiting, and CORS for 2026-era setups.

I followed the same trend in ACF 6.8 makes WordPress an operation target for AI agents and WordPress 7.0 keeps the AI foundation, drops real-time collaboration.
The Abilities API and MCP Adapter expose WordPress operations to AI agents, and the AI Client and Connectors API move external service credentials toward Core.
This post covers the layer underneath all of that: the HTTP entrance and WordPress user permissions.

Before closing /wp-json/, separate the admin screen from external integrations

The official WordPress FAQ says not to disable the REST API itself, because admin functionality breaks.
In practice the block editor and plugin management call the REST API, so disabling the whole API breaks the admin screen first.

On the other hand, if a site has no reason to allow anonymous access, rest_authentication_errors can return 401 for unauthenticated REST API requests.
Before closing the whole API, sort out which routes the public site, the admin screen, external clients, webhooks, and AI integrations actually use.

If a Next.js or Astro frontend only reads published posts, /wp/v2/posts has a reason to stay open.
For membership sites, internal CMS installs, and posting-only backends, anonymous /wp/v2/users and the search endpoints have little reason to remain.
Even in current WordPress, /wp/v2/users enumerates users who have published posts, with no authentication.
On top of that, ?search= also matches email addresses, so an attacker can narrow down an email one character at a time — filed as Trac #53784 in 2021 and still unfixed.
Note that WordPress core does not treat usernames as secrets, so rather than trying to hide enumeration completely, the working assumption is an authentication layer that holds up even when usernames are known.
In setups where external tools operate WordPress — ACF, MCP Adapter — the blast radius is decided by which WordPress user the request executes as.

There is one more thing: the REST API index at /wp-json/.
It lists plugin-added routes, authentication methods, and namespaces, so to an attacker it reads like a site-specific feature inventory.
Even when it cannot be fully hidden, separating externally visible routes from authenticated routes in your logs makes later investigation easier.

Application Passwords are convenient but run as Basic auth

Since WordPress 5.6, Application Passwords are the standard external authentication for the REST API.
The official administration handbook describes them as per-user, per-application credentials that can be revoked individually and cannot be used for normal wp-admin login.

Application Passwords fit CI jobs, migration scripts, and external posting tools — processes where no human goes through a login screen.
Requests send Authorization: Basic ....
HTTPS is not a recommendation but a precondition: on non-HTTPS sites, Application Passwords are disabled by default.
In Apache CGI environments the Authorization header sometimes never reaches PHP, and the official FAQ documents the fixes — SetEnvIf Authorization for Apache, fastcgi_pass_header Authorization for nginx.
Reverse-proxy setups can drop the header the same way, and in that case you get 401 responses even with correct credentials.

Reusing one Application Password on an administrator account means a leak is worth administrator privileges.
If an integration only creates posts, give it a posting user; if it only uploads media, give it only those capabilities.
The handbook’s wp user application-password list shows last_used and last_ip, so delete the ones nothing is using.

JWT is for frontend app logins, but it is not a WordPress core feature — it depends on a plugin.
If you hold a secret key, token expiry, refresh tokens, and a revocation list, you take on token management operations in addition to WordPress user permissions.
Depending on a plugin also means vulnerability tracking happens per plugin.
In CVE-2026-14262, published July 11, 2026, the auth endpoint of Simple JWT Login 3.6.6 and earlier accepted a payload parameter that injected an administrator’s email into token generation, letting a Subscriber take over an administrator session.
The similarly named JWT Authentication for WP REST API has no registered CVE at the moment.
Do not lump them together as “the JWT plugin” — which one is installed decides where to check.

Split the methods: Application Passwords for one-off external scripts, JWT for browser app logins, nonces for operations inside the admin screen.
Every additional authentication method adds revocation and log-review procedures.

CORS is not a substitute for authentication

The WordPress REST API is designed not to validate the incoming Origin header.
The official FAQ explains that WordPress chose nonces, not CORS, to prevent CSRF, and that public REST API endpoints can be accessed from any site.

In other words, narrowing CORS does nothing to a curl request hitting the API directly.
CORS is the mechanism by which a browser stops cross-origin JavaScript; it is not the API’s own authorization decision.
If a design writes with cookies from a logged-in browser, Access-Control-Allow-Credentials: true and the allowed-origin handling become the targeted spot.

Avoid a setup that returns Access-Control-Allow-Origin: * while accepting authenticated operations at the same entrance.
With multiple legitimate frontends, match the incoming origin against an allowlist and echo it back only on a match.
The implementation is in the official FAQ: remove the default rest_send_cors_headers and return your own headers from the rest_pre_serve_request hook.
Even then, every write route must be stopped by one of: a nonce, Application Passwords, JWT, or a capability check.

The same misunderstanding caused trouble in local tools too.
In the OpenCode unauthenticated RCE post, the assumption “it is safe because it runs locally” stopped holding for an HTTP server with unrestricted CORS.
WordPress is a public server, so the conditions differ, but the part that CORS cannot stand in for authorization is the same.

Stop abuse before it reaches PHP

WordPress core ships no rate limiting for the REST API, so it has to be added somewhere.
Brute force against the REST API, user enumeration, and hammering the search endpoints all trigger a WordPress bootstrap the moment they reach PHP, even if the request is then rejected.
Put one layer in front of PHP: nginx limit_req, Apache mod_evasive, a WAF like Cloudflare or Sucuri, or the hosting provider’s limits.
As a reference point, the WP Admin Lab post gives a multi-layer example of 5 requests/minute for authentication endpoints and 60 requests/minute for public reads.

However, corporate networks, mobile carriers, anything behind Cloudflare, and school or shop Wi-Fi make many legitimate users appear as one egress IP.
Per-IP limits throttle all the legitimate users in those places at once.
Be strict on JWT token issuance and Application Passwords write routes, and push public post reads out to caches and CDNs.

If the server returns HTTP 429, return Retry-After with it.
Legitimate clients can read the retry interval, and in the logs the event reads as “throttled on purpose” instead of “went down”.
If the API consumer is your own script, exponential backoff on 429 alone changes the server-side load noticeably.

Create a dedicated user for AI integrations

WordPress 7.0’s AI Client and Connectors API, and ACF 6.8’s Abilities API integration, all make WordPress easier to operate from external tools.
Core is addressing credential exposure too — the Connectors API masks stored API keys both in the settings screen and in REST API responses.
Still, as external tools multiply, REST API, Application Passwords, OAuth, MCP Adapter, and plugin-specific endpoints get harder to tell apart.

The user handed to an external AI tool should not be your everyday administrator account.
Creating draft posts, uploading media, operating custom post types, changing settings, and managing plugins are separate things.
Running an external integration as a user with manage_options or install_plugins means a leaked API token is a leaked site administration.

The official MCP Adapter introduction says the same: create a dedicated user with restricted permissions for production MCP access, and prefer read-only abilities on public endpoints.
Self-hosted MCP Adapter authentication is currently Application Passwords or JWT; OAuth 2.1 is only available on the WordPress.com side.
That means the credentials used for AI integrations also end up issued and revoked as Application Passwords.

Vertex Addons for Elementor’s CVE that let Subscribers activate arbitrary plugins was a vulnerability through admin-ajax.php, not the REST API.
That one was an implementation bug that continued processing without acting on the result of current_user_can('install_plugins') — an authorization check that exists but does not take effect lets the operation proceed.
The REST API works the same way: if permission_callback is only is_user_logged_in(), check whether a Subscriber can perform the operation.
There is a real case. Post SMTP, installed on 400,000 sites, had CVE-2025-24000: its email-log permission_callback only checked login state, so registering as a Subscriber was enough to fetch the email logs, including password reset emails addressed to administrators.
The official documentation also says to check per-operation permissions with current_user_can instead of checking login state.

The boundaries for sites that keep the REST API

A site that uses WordPress as a public read API does not fully close anonymous GET.
Instead, it reviews the user list, search, custom plugin routes, and the permission_callback on write routes.
If the browser-facing frontends are fixed, narrow the CORS allowed origins, and allow Access-Control-Allow-Credentials only when cookie authentication is in use.

A site where external tools manage posts and media issues one Application Password per integration.
Instead of reusing the administrator’s own credentials, it sets up a dedicated user, HTTPS, a check that the Authorization header passes through, and a revocation procedure as one package.

On a site operated by AI agents and automation tools, the /wp-json/ routes, Application Passwords, JWT plugins, MCP Adapter, and AI plugins each grow in a different place.
They only differ at the entrance — what executes in the end is a WordPress user’s capability, so they belong on a single list.

References: