Ask ten WordPress developers about WordPress REST API security and you will get ten different instincts. Some want it switched off. Some have never looked at what it exposes. The truth is calmer than either: the REST API is a normal, useful part of WordPress, and a few specific things about it need attention.
This WordPress REST API security guide covers what is public by default, how requests are authenticated, how to write routes that are safe, and how to restrict access without breaking the editor.
What the REST API Is and Why It Is on by Default
The REST API lets other software talk to your site over HTTP and get answers in JSON. The block editor uses it to save posts. Mobile apps, headless front ends and many plugins use it too.
According to the REST API handbook's key concepts, the entry point is /wp-json/, which returns an index listing the available routes. A route is an address, and an endpoint is one HTTP method on that address. For example, wp/v2/posts has a GET endpoint that lists posts and a POST endpoint that creates them, and only authenticated requests can use the POST. If your site uses plain permalinks, the same index sits at ?rest_route=/.
You can see it for yourself. Open yoursite.com/wp-json/ in a browser. Everything you see there, an attacker can see too.
That is not a flaw. It is the same data your public pages show, in a different format. The trouble starts in three places:
- Data that is public by default but should not be.
- Weak or missing authentication on routes that change things.
- Custom routes written without a proper permission check.
What Is Public by Default
Reading public content needs no sign-in. Anyone can request your published posts and pages, categories, tags and media records, the same way anyone can read them on the website.
Published content is not secret, so there is nothing to protect there. The things worth a second look are:
| Exposed item | Why it matters |
|---|---|
The index at /wp-json/ | Lists every route, including those added by plugins, which reveals what is installed |
/wp-json/wp/v2/users | Can list user names and slugs to anyone |
| Custom plugin routes | May expose data or actions if their permission checks are weak |
| Media and attachments | Shows file addresses, including ones you may not link anywhere |
The Users Endpoint and User Enumeration
The one that gets the most attention is the users endpoint. A request to /wp-json/wp/v2/users can return a list of users along with their name and slug. The handbook's users reference shows an unauthenticated request to that address as its example. Sensitive fields such as email and roles only appear in the edit context, which needs authentication, but the slug is available and often matches the sign-in name.
That is called user enumeration. A known username halves the work of a password-guessing attack, because the attacker only needs the password. We explain how it works and how to stop it in user enumeration explained, and the same idea applies to ?author=1 links on the front end.
The block-or-not decision is simple: public visitors have no reason to list your users. Block the list for visitors who are signed out, and leave it working for signed-in users so the editor and admin tools are unaffected.
How Authentication Works
Authentication is the heart of WordPress REST API security. The handbook's authentication page lists the main options. Three are worth knowing.
Cookie Authentication
This is the default. If you are signed in to WordPress and your browser sends the session cookie, the request runs as you. To protect against cross-site request forgery, WordPress also requires a nonce created with the wp_rest action. Send it in the X-WP-Nonce header or the _wpnonce parameter.
If the nonce is missing or wrong, WordPress treats the request as logged out, even though the cookie is valid. That is why a script that works in the browser console can suddenly return 401 or only public data.
wp_localize_script( 'myplugin-app', 'mypluginRest', array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
We cover how nonces behave, including how long they last and what caching does to them, in our article on the WordPress nonce.
Application Passwords
Outside apps and scripts cannot use your browser cookie. Since WordPress 5.6, they can use application passwords instead: a separate password per app, created on the user's profile page and sent with HTTP Basic Auth. The handbook notes that credentials are passed to REST requests served over HTTPS.
Application passwords are convenient, but they matter for security: they are a way into the site that does not go through the normal sign-in page. That includes the page where you may have set up two-factor authentication. If you do not use them, switch them off. If you do, review them often. Our article on WordPress application passwords goes through both sides.
Authentication Plugins
The handbook also mentions plugins that add other methods, such as OAuth or JSON Web Tokens. Those bring their own security questions, so choose a well-maintained one and keep it updated.
Write Safe Routes With permission_callback
If you build custom routes, this section matters more than any other. The handbook on adding custom endpoints describes permission_callback as the function that checks whether the user can perform the action before your main callback runs. It should return a boolean or a WP_Error.
What Missing and Always-True Both Mean
Since WordPress 5.5, leaving out permission_callback triggers a _doing_it_wrong notice saying the route definition is missing the required argument. That notice is a warning for you as a developer. Treat it as an error to fix, not noise to hide.
The handbook says that for a genuinely public endpoint you should use __return_true. That is correct for public data, such as a read-only list of store locations. It is a disaster for anything that writes.
Key takeaway:
__return_trueis a statement that anyone on the internet may call this route. Never use it on a route that creates, changes or deletes anything, or that returns private data.
A Safe Pair of Routes
Here is a public read route and a protected write route side by side:
add_action( 'rest_api_init', function () {
// Public, read-only. Fine to leave open.
register_rest_route( 'myplugin/v1', '/locations', array(
'methods' => 'GET',
'callback' => 'myplugin_get_locations',
'permission_callback' => '__return_true',
) );
// Writes data. Needs a real capability check.
register_rest_route( 'myplugin/v1', '/locations', array(
'methods' => 'POST',
'callback' => 'myplugin_create_location',
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
'args' => array(
'name' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
) );
} );
A few habits that keep routes safe:
- Check a capability, not just a login. Being signed in is not the same as being allowed. A subscriber is signed in too.
- Check the specific object. If a route edits post 42, check
current_user_can( 'edit_post', 42 ), not justedit_posts. - Define and sanitize your arguments. Use the
argsarray to set types, required fields and sanitize callbacks, and validate values. - Escape and limit what you return. Do not return a whole user or post object when two fields will do.
- Return proper errors. A
WP_Errorwith astatusof401or403tells clients what happened.
Restricting Endpoints Without Breaking Things
Sometimes you want to close the API to visitors who are not signed in. There are two common hooks for this, and both are easy to get wrong.
rest_authentication_errors
This filter runs when WordPress checks authentication. According to the hook reference, the incoming value is null if no method has decided yet, a WP_Error for a failure, or true for success. Your callback should pass through anything that is not null, and only then apply your own rule.
add_filter( 'rest_authentication_errors', function ( $result ) {
// Another method already decided; respect it.
if ( null !== $result ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
'You must be signed in to use this.',
array( 'status' => 401 )
);
}
return $result;
} );
That blocks every REST request from signed-out visitors. It is blunt, and it can break more than you expect: contact forms that submit through the REST API, public search or filter widgets, headless front ends, and some plugins that call their own routes from public pages.
A more careful version checks the requested route and only blocks the ones you mean to, or allows a list of known public namespaces.
rest_endpoints
This filter receives the full list of registered endpoints. You can remove specific ones before the server uses them:
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
}
return $endpoints;
} );
This is precise: only the routes you name are removed, and only for visitors who are signed out.
A Safe Way to Make Changes
- Never block signed-in users from the API. The block editor needs it, and so does most of wp-admin.
- Test the editor after every change. Open a post, edit, save, and add an image.
- Test the front end as a visitor. Forms, search and filters are where hidden breakage tends to show up.
- Prefer removing specific routes to blocking everything. It is easier to explain and easier to undo.
- Keep a way back. A staging copy or a fresh backup lets you undo a change that locks you out.
Pro tip: If your site truly has no public use for the API, blocking it for signed-out visitors is reasonable. If you are not sure, remove the users routes and leave the rest.
Rate Limiting and Abuse
The REST API has no built-in rate limiting. Anyone can send as many requests as your server will accept, and some routes are expensive: search, large queries and anything that writes to the database.
You have a few options, from broad to specific:
- At the edge. A CDN or web application firewall can limit requests per address before they reach WordPress. This is the best place for it, because the traffic never touches PHP.
- At the server. Nginx and Apache both support request limits for specific paths such as
/wp-json/. - In your own routes. For sensitive custom routes, count requests per user or IP address using a transient and return a
429when the limit is passed. - By reducing what is expensive. Cap
per_pagevalues, require authentication for heavy queries and cache public responses.
Be careful with IP-based limits behind a proxy or CDN. If WordPress sees every request coming from the proxy, one noisy visitor can lock out everyone. Make sure the real visitor address is read from the right header.
How Stack Guard Helps
You can apply everything above by hand, and for custom routes you have to. For the common hardening steps, Stack Guard covers a few without code.
On the Free plan, Hardening includes Block user enumeration. For visitors who are not signed in, it removes the /wp/v2/users routes, the users sitemap and author details in embeds, and sends ?author= requests to the home page. Signed-in users are not affected, so the block editor and other admin tools keep working. The information disclosure documentation has the details.
On Pro, Disable application passwords turns off the feature site wide. It suits sites where no app or integration signs in with one. Both options sit in the Hardening tab, so you can review them together.
If you build on top of Stack Guard, the developer reference lists the actions, filters, constants and WP-CLI commands you can use to connect your own tooling. For a wider checklist beyond the API, see our WordPress hardening checklist.
A WordPress REST API Security Checklist
- Open
/wp-json/and/wp-json/wp/v2/userswhile signed out. Note what you see. - Block user enumeration for visitors who are signed out.
- Give every custom route a
permission_callback, with a real capability check on anything that writes. - Use
__return_trueonly for read-only, public data. - Send the
wp_restnonce with cookie-authenticated requests from JavaScript. - Disable application passwords if nothing uses them, and review them if something does.
- Restrict routes with
rest_endpointsorrest_authentication_errors, and test the editor and front end afterwards. - Add rate limiting at the edge or server.
- Keep plugins updated. Vulnerable plugin routes are a common way in, as we explain in WordPress plugin vulnerabilities.
Good WordPress REST API security is mostly small, specific choices: a closed users list, a real permission check on every route and an up-to-date plugin list. You do not need to switch the API off to do any of it.