Type something to search...
How to secure the WordPress REST API?

How to secure the WordPress REST API?

To secure the WordPress REST API, don't switch it off entirely, because the block editor and many plugins depend on it. Instead, require authentication for endpoints that don't need to be public, hide the users endpoint from anonymous visitors, give every custom endpoint a proper permission_callback with validated and sanitized arguments, control who can create application passwords, and add rate limiting at the server or firewall level. Together, these steps keep the API useful for your site while shrinking what strangers can see and do.

The REST API is a powerful part of WordPress, and like any public interface it deserves some attention. In this guide, you'll learn what the REST API exposes by default, which risks are real and which are overstated, and how to lock it down step by step with code you can drop into a custom plugin.

What the WordPress REST API Exposes by Default

The REST API lives at /wp-json/ on every WordPress site. Visit https://example.com/wp-json/ and you'll see a JSON index of every registered route, including routes added by plugins.

For logged-out visitors, core endpoints mostly return data that's already public on your site:

  • Posts and pages: /wp-json/wp/v2/posts and /wp-json/wp/v2/pages return published content.
  • Categories, tags, and media: Taxonomy terms and attachment details for public items.
  • Users: /wp-json/wp/v2/users lists users who have published posts, including their user ID, display name, and slug.
  • Search and comments: Public search results and approved comments.
  • Site information: The site name, description, URL, and the list of available routes.

Anything private, such as drafts, private posts, email addresses, or settings, requires authentication and the right capabilities. WordPress core checks permissions carefully on its own endpoints.

Where the Real Risks Come From

In practice, REST API security problems usually come from three places:

  1. Plugin endpoints with weak permission checks: A plugin registers a route that changes data but uses 'permission_callback' => '__return_true', or checks the wrong capability. This is the most common source of serious REST API vulnerabilities.
  2. Information disclosure: The users endpoint and the route index reveal usernames (via slugs) and the list of plugins with REST routes, which helps attackers plan brute-force attempts or target known plugin bugs.
  3. Abuse of public endpoints: Search, comment, and form submission endpoints can be hammered by bots, causing load or spam.

Why You Shouldn't Disable the REST API Completely

Older security guides often recommended disabling the REST API entirely. That advice no longer fits a modern WordPress site:

  • The block editor needs it: The editor, the Site Editor, and the widgets screen load and save content through the REST API.
  • Plugins depend on it: Contact Form 7, WooCommerce, Jetpack, many SEO plugins, and most headless or app integrations use REST routes, some of them from the public front end.
  • WordPress core features use it: Site Health checks, embeds, and parts of the admin rely on it.

The better approach is to restrict it, not remove it. Logged-in users with the right capabilities keep full access, and anonymous visitors only get the specific public routes your site actually needs.

Step 1: Require Authentication for Anonymous Requests

The simplest restriction is to reject every REST request from logged-out visitors. The WordPress REST API Handbook documents this approach using the rest_authentication_errors filter. Add it to a custom plugin or your child theme's functions.php:

<?php
/**
 * Require authentication for all REST API requests.
 */
function sajjad_rest_require_auth( $result ) {
    // Respect a previous authentication result (success or error).
    if ( true === $result || is_wp_error( $result ) ) {
        return $result;
    }

    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            __( 'You are not currently logged in.', 'sajjad' ),
            array( 'status' => 401 )
        );
    }

    return $result;
}
add_filter( 'rest_authentication_errors', 'sajjad_rest_require_auth' );

This is very effective, but it's also blunt. Any front-end feature that calls the REST API without being logged in stops working, including many contact forms, live search features, WooCommerce Blocks on the cart and checkout pages, and embeds of your posts on other sites.

A Safer Version With an Allowlist

For most sites, it's better to block anonymous access by default and allow a short list of routes that your front end needs. The rest_pre_dispatch filter gives you the request object, so you can check the route:

<?php
/**
 * Block anonymous REST requests except for an allowlist of route prefixes.
 */
function sajjad_rest_allowlist( $result, $server, $request ) {
    // Another filter already handled the request, or the user is logged in.
    if ( null !== $result || is_user_logged_in() ) {
        return $result;
    }

    $route = $request->get_route();

    // Route prefixes that anonymous visitors are allowed to use.
    $allowed_prefixes = array(
        '/oembed/1.0',           // Embeds of your posts on other sites.
        '/contact-form-7/v1',    // Contact Form 7 submissions, if you use it.
        '/wc/store',             // WooCommerce Store API for cart and checkout blocks.
    );

    foreach ( $allowed_prefixes as $prefix ) {
        if ( str_starts_with( $route, $prefix ) ) {
            return $result;
        }
    }

    return new WP_Error(
        'rest_forbidden',
        __( 'Authentication is required to access this endpoint.', 'sajjad' ),
        array( 'status' => rest_authorization_required_code() )
    );
}
add_filter( 'rest_pre_dispatch', 'sajjad_rest_allowlist', 10, 3 );

Adjust the allowlist to match your site. If you're not sure which routes your front end uses, open your browser's developer tools, go to the Network tab, filter by wp-json, and click around your site while logged out. Every route you see there needs to be allowed.

If you run a headless WordPress site where a separate front end reads posts through the API, you'll also need to allow routes such as /wp/v2/posts and /wp/v2/pages, or authenticate your front end's requests.

How Authentication Works Here

It helps to understand why is_user_logged_in() works inside these filters. The REST API supports a few authentication methods:

  • Cookie authentication: Used by the block editor and your own JavaScript. It requires a valid login cookie and a nonce sent in the X-WP-Nonce header. Without the nonce, WordPress treats the request as anonymous, which protects you against cross-site request forgery.
  • Application passwords: Built into WordPress since 5.6. External apps send a username and application password over HTTPS using Basic Authentication.
  • Plugin-based methods: OAuth or JWT plugins for more advanced integrations.

By the time rest_pre_dispatch runs, WordPress has already processed these, so the current user is set correctly.

Step 2: Hide the Users Endpoint From Anonymous Visitors

Even if you don't restrict the whole API, it's worth hiding the users endpoints from logged-out visitors. They reveal user slugs, which often match login usernames. You can remove the routes for anonymous requests with the rest_endpoints filter:

<?php
/**
 * Remove user endpoints for logged-out visitors.
 */
function sajjad_rest_hide_users( $endpoints ) {
    if ( is_user_logged_in() ) {
        return $endpoints;
    }

    unset( $endpoints['/wp/v2/users'] );
    unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );

    return $endpoints;
}
add_filter( 'rest_endpoints', 'sajjad_rest_hide_users' );

Logged-in users keep access, which matters because the block editor uses the users endpoint to populate the author selector. User enumeration can also happen through author archives and login error messages, so if this is a concern for you, it's worth tackling those as well.

Step 3: Write Secure Custom Endpoints

If you or your developer register custom REST routes, this is where your attention matters most. Since WordPress 5.5, every route must declare a permission_callback. Here's an example of a well-protected endpoint that lets editors update a custom setting:

<?php
/**
 * Register a protected REST route.
 */
function sajjad_register_rest_routes() {
    register_rest_route(
        'sajjad/v1',
        '/featured-notice',
        array(
            'methods'             => WP_REST_Server::EDITABLE, // POST, PUT, PATCH
            'callback'            => 'sajjad_update_featured_notice',
            'permission_callback' => 'sajjad_can_edit_notice',
            'args'                => array(
                'message' => array(
                    'type'              => 'string',
                    'required'          => true,
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => function ( $value ) {
                        return is_string( $value ) && strlen( $value ) <= 200;
                    },
                ),
                'enabled' => array(
                    'type'    => 'boolean',
                    'default' => true,
                ),
            ),
        )
    );
}
add_action( 'rest_api_init', 'sajjad_register_rest_routes' );

/**
 * Only users who can edit others' posts may change the notice.
 */
function sajjad_can_edit_notice( WP_REST_Request $request ) {
    return current_user_can( 'edit_others_posts' );
}

/**
 * Save the notice. Arguments are already validated and sanitized.
 */
function sajjad_update_featured_notice( WP_REST_Request $request ) {
    $data = array(
        'message' => $request->get_param( 'message' ),
        'enabled' => (bool) $request->get_param( 'enabled' ),
    );

    update_option( 'sajjad_featured_notice', $data );

    return rest_ensure_response( $data );
}

Follow these rules for every custom endpoint:

  1. Never use __return_true for write operations: Only use it for routes that are genuinely public and read-only.
  2. Check capabilities, not roles: Use current_user_can() with a specific capability like edit_posts or manage_options, rather than checking role names.
  3. Check object-level permissions: If a route edits a specific post, check current_user_can( 'edit_post', $post_id ), not just a general capability.
  4. Define and sanitize every argument: Use type, validate_callback, and sanitize_callback in args so bad input is rejected before your callback runs.
  5. Use $wpdb->prepare() for custom queries: Never put request values directly into SQL.
  6. Escape output: If your endpoint returns HTML, escape it. Return plain data where possible and let the client render it.
  7. Return only what's needed: Don't include email addresses, internal IDs, or settings in responses unless the caller needs them.

Calling Your Endpoint Safely From JavaScript

When your own front-end or admin JavaScript calls a protected route, use the wp-api-fetch package, which handles the nonce for you in the admin. On the front end, pass a nonce created with wp_create_nonce( 'wp_rest' ):

<?php
function sajjad_enqueue_notice_script() {
    wp_enqueue_script(
        'sajjad-notice',
        plugins_url( 'notice.js', __FILE__ ),
        array( 'wp-api-fetch' ),
        '1.0.0',
        array( 'in_footer' => true )
    );
}
add_action( 'admin_enqueue_scripts', 'sajjad_enqueue_notice_script' );
// notice.js
import apiFetch from "@wordpress/api-fetch";

apiFetch({
  path: "/sajjad/v1/featured-notice",
  method: "POST",
  data: { message: "Sale ends Friday", enabled: true },
}).then((response) => {
  console.log("Saved", response);
});

If you load this file without a build step, use the global wp.apiFetch instead of the import statement. In the admin, WordPress registers the REST nonce middleware for wp-api-fetch automatically, so cookie authentication works without extra code.

Step 4: Manage Application Passwords

Application passwords let external tools authenticate with the REST API. They're useful for integrations, but every one is a long-lived credential that bypasses two-factor authentication on your main login. Review them regularly under Users > Profile, in the Application Passwords section, and revoke any you don't recognise.

If your site doesn't use any integrations that need them, you can disable the feature completely:

<?php
// Disable application passwords for all users.
add_filter( 'wp_is_application_passwords_available', '__return_false' );

Or allow them only for administrators:

<?php
function sajjad_limit_application_passwords( $available, $user ) {
    return user_can( $user, 'manage_options' );
}
add_filter( 'wp_is_application_passwords_available_for_user', 'sajjad_limit_application_passwords', 10, 2 );

WordPress only accepts application passwords over HTTPS by default, which is another reason to make sure your whole site runs on HTTPS.

Step 5: Add Rate Limiting and Firewall Rules

WordPress core doesn't rate limit REST requests. Public endpoints like search or form submissions can be flooded by bots. Rate limiting is best handled before requests reach PHP:

  • Web application firewalls: Services like Cloudflare, Sucuri, or your host's firewall can rate limit paths under /wp-json/.
  • Security plugins: Wordfence includes rate limiting rules for crawlers and requests, which also apply to REST traffic.
  • Nginx rate limiting: If you manage the server, you can limit request rates per IP.

Here's an Nginx example that limits each IP to a modest rate on REST routes. The limit_req_zone line goes in the http block (for example in /etc/nginx/nginx.conf), and the location goes in your site's server block:

# In the http block
limit_req_zone $binary_remote_addr zone=wp_rest:10m rate=10r/s;

# In your site's server block
location ^~ /wp-json/ {
    limit_req zone=wp_rest burst=20 nodelay;
    try_files $uri $uri/ /index.php?$args;
}

Start with generous limits and tighten them only after watching your logs. The block editor sends a burst of requests when it loads, and you don't want to throttle your own editors. If your site is behind a proxy or CDN, configure Nginx to use the real visitor IP, or everyone will share the same limit.

Step 6: Use a Plugin If You Prefer

If you'd rather not write code, a few well-known plugins can restrict REST API access:

  • Disable WP REST API: A lightweight plugin that blocks REST access for logged-out users.
  • Solid Security: Includes a REST API setting that can restrict access to logged-in users.
  • Wordfence: Doesn't restrict the API directly, but its firewall and rate limiting help protect REST endpoints from abuse.

Whichever plugin you choose, test your contact forms, search, checkout, and embeds while logged out afterwards.

Testing Your REST API Security

After making changes, test from a logged-out context with curl:

# Should return 401 if anonymous access is restricted
curl -i https://example.com/wp-json/wp/v2/posts

# Should return 404 (no route) or 401 if the users endpoint is hidden
curl -i https://example.com/wp-json/wp/v2/users

# An allowed route should still work
curl -i "https://example.com/wp-json/oembed/1.0/embed?url=https://example.com/sample-post/"

Then log in and make sure the block editor loads, saves, and shows the author selector. Finally, test every public feature that uses REST, such as forms and WooCommerce checkout, in a private window.


FAQ: Securing the WordPress REST API

No. The block editor, Site Editor, and many plugins depend on it. Restrict anonymous access and protect sensitive endpoints instead of disabling it completely.

Core endpoints are well protected and only show public data to anonymous visitors. The bigger risks are plugin endpoints with weak permission checks and information disclosure, such as usernames from the users endpoint.

It's a function you pass to register_rest_route() that decides whether the current user may call the route. It should return true only when the user has the right capability, and it's required for every route since WordPress 5.5.

Remove the /wp/v2/users routes for logged-out visitors with the rest_endpoints filter, as shown in this guide. Logged-in users keep access so the block editor still works.

It can, if the form plugin submits through the REST API. Contact Form 7 does, for example. Use an allowlist so those routes stay available to logged-out visitors.

They're safe when used over HTTPS and managed carefully, but they bypass two-factor authentication. Revoke unused ones, limit them to trusted users, or disable them if you don't need integrations.

Use a web application firewall like Cloudflare or Sucuri, a security plugin with rate limiting such as Wordfence, or Nginx limit_req rules on the /wp-json/ path.


Conclusion

Securing the WordPress REST API is about restriction rather than removal. Keep the API running so the block editor and your plugins work, but block anonymous access to routes that don't need to be public, hide the users endpoints from strangers, and make sure every custom route checks capabilities and validates its input. Those changes alone close the most common gaps.

From there, tidy up application passwords, add rate limiting at your firewall or web server, and test your public features after every change. The REST API is one of WordPress's most useful features, and with a few careful controls in place, it doesn't have to be one of your weak points.

Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
What is the difference between posts and pages in WordPress?

What is the difference between posts and pages in WordPress?

The main difference between posts and pages in WordPress is that posts are timely, dated entries that appear in your blog feed, archives, and RSS fee

Dive Deeper