How to Secure Your WordPress REST API in 2026
Secure your WordPress REST API by disabling unauthenticated access to sensitive endpoints, blocking user enumeration through the /wp-json/wp/v2/users route, enforcing rate limiting on all API requests, restricting Cross-Origin Resource Sharing (CORS) headers to trusted domains and logging every API call for real-time threat detection.
- Block user enumeration on
/wp-json/wp/v2/usersand?rest_route= - Require authentication for all non-public REST API endpoints
- Enforce rate limiting at the server and application level
- Lock down CORS to your own domains only
- Disable XML-RPC to close the parallel attack surface
What Is the WordPress REST API and Why Is It a Security Risk?
The WordPress REST API (accessible at /wp-json/) provides a standardized interface for external applications, themes and plugins to read and write site data over HTTP. Introduced in WordPress 4.7, it powers everything from the Gutenberg block editor to headless front ends built on React or Vue. The problem: WordPress ships with dozens of endpoints enabled by default, many of which expose data that attackers use for reconnaissance.
Every WordPress site running a default configuration publicly exposes its REST API index at /wp-json/wp/v2/. This is not a theoretical risk. In March 2026, CVE-2026-3506 demonstrated how an access control bypass in a popular chatbot plugin allowed unauthenticated attackers to read private post content through the REST API. The vulnerability affected over 300,000 active installations before a patch shipped.
Default Endpoints That Expose Your Site
A stock WordPress installation registers these high-risk endpoints with no authentication requirement:
/wp-json/wp/v2/users: returns usernames, user IDs, author URLs and Gravatar hashes. This is the single most exploited REST API endpoint because it hands attackers a list of valid login targets./wp-json/wp/v2/posts: exposes published content, author IDs and revision metadata. Draft and private posts are filtered by default but plugin conflicts can leak them (as CVE-2026-3506 proved)./wp-json/wp/v2/settings: normally restricted to administrators, but misconfiguredpermission_callbackfunctions in custom endpoints can expose site settings./wp-json/wp/v2/search: allows unauthenticated content discovery across post types./wp-json/(root index): reveals every registered route, namespace and endpoint method. This is a full map of your API attack surface.
You can verify your own exposure right now. Open a private browser window and visit yourdomain.com/wp-json/wp/v2/users. If you see a JSON array of user objects, your site is leaking credentials data to the public internet.
How Attackers Exploit the REST API
Attackers follow a predictable kill chain when targeting WordPress REST APIs:
- Reconnaissance: automated scanners hit
/wp-json/wp/v2/usersand/?author=1to harvest usernames. Tools like WPScan and custom Python scripts enumerate every user account in seconds. - Credential stuffing: harvested usernames feed into brute force attacks against
wp-login.php, XML-RPC’ssystem.multicallmethod and the REST API’s own authentication endpoints. - Privilege escalation: once inside a low-privilege account, attackers probe custom REST API routes for broken access controls. The OWASP API Security Top 10 lists Broken Object Level Authorization (BOLA) as the number-one API vulnerability for good reason.
- Data exfiltration or lateral movement: compromised API access can enable server-side request forgery (SSRF) through vulnerable plugins, turning your WordPress server into a proxy for attacks on internal infrastructure.
The fix is not to disable the REST API entirely. The block editor depends on it. Disabling it breaks core WordPress functionality. Instead, you harden it methodically: restrict who can access what, limit request volume and monitor everything.
7 Steps to Harden Your WordPress REST API
We use these hardening steps on every client site we manage at Quake Media. Each snippet is production-tested on WordPress 6.7+ running PHP 8.2 or later.
1. Block Unauthenticated User Enumeration
User enumeration is the most common REST API exploit. This snippet removes the /wp-json/wp/v2/users endpoint for unauthenticated visitors while preserving it for logged-in administrators. Add this to your theme’s functions.php or a custom security plugin.
<?php
/**
* Block unauthenticated access to the Users endpoint.
* Prevents user enumeration via /wp-json/wp/v2/users
* while keeping the endpoint available for admin use.
*
* Tested on WordPress 6.7.x / PHP 8.2+
*/
add_filter( 'rest_endpoints', function ( $endpoints ) {
// Only restrict for non-authenticated requests
if ( ! is_user_logged_in() ) {
// Remove users endpoint entirely for public visitors
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;
});You should also block the older ?author=N enumeration vector. Add this to the same file:
<?php
/**
* Block author archive enumeration (?author=1, ?author=2, etc.)
* Redirects unauthenticated author queries to the homepage.
*/
add_action( 'template_redirect', function () {
if ( ! is_user_logged_in() && isset( $_GET['author'] ) ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
});After deploying these two snippets, test with curl -s https://yourdomain.com/wp-json/wp/v2/users | jq . from an unauthenticated session. You should receive an empty response or a 403 error instead of a user list.
2. Restrict REST API Access for Non-Authenticated Users
This is the most impactful single change you can make. It forces authentication on every REST API request while whitelisting specific public endpoints that your site needs (such as the Gutenberg block data endpoints or Contact Form 7).
<?php
/**
* Require authentication for all REST API requests.
* Whitelists specific public endpoints needed for site functionality.
*
* Hook: rest_authentication_errors
* Priority: 99 (runs after other auth checks)
*/
add_filter( 'rest_authentication_errors', function ( $result ) {
// Pass through if another auth handler already denied access
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
// Allow authenticated users full access
if ( is_user_logged_in() ) {
return $result;
}
// Define public endpoint whitelist
$public_routes = array(
'/wp/v2/posts', // Public blog content
'/wp/v2/pages', // Public pages
'/wp/v2/categories', // Public taxonomy
'/wp/v2/tags', // Public taxonomy
'/contact-form-7/', // CF7 form submissions
'/oembed/', // Embed discovery
);
$request_route = $_SERVER['REQUEST_URI'] ?? '';
// Check if the current route matches any whitelisted pattern
foreach ( $public_routes as $route ) {
if ( false !== strpos( $request_route, $route ) ) {
return $result;
}
}
// Block everything else for unauthenticated users
return new WP_Error(
'rest_not_authorized',
__( 'Authentication is required to access this endpoint.' ),
array( 'status' => 401 )
);
}, 99 );Adjust the $public_routes array to match your site’s requirements. If you run a headless WordPress setup, you will need a broader whitelist or a token-based approach (covered in step 5).
3. Implement Rate Limiting on API Endpoints
Rate limiting prevents brute force attacks and automated scraping. The best approach is layered: server-level limits handle volume; application-level limits add granularity.
Nginx configuration (add to your server block):
# Define a rate limit zone for REST API requests
# 10 requests per second per IP with a 10MB shared memory zone
limit_req_zone $binary_remote_addr zone=wp_rest_api:10m rate=10r/s;
server {
# Apply rate limiting to wp-json endpoints
location /wp-json/ {
limit_req zone=wp_rest_api burst=20 nodelay;
limit_req_status 429;
# Pass to PHP handler
try_files $uri $uri/ /index.php?$args;
}
}Apache .htaccess (requires mod_ratelimit or mod_evasive):
# Rate limit REST API requests via mod_headers + mod_rewrite
# This approach sets rate limit headers; enforce with a WAF or plugin
<IfModule mod_rewrite.c>
RewriteEngine On
# Block direct access to wp-json user enumeration
RewriteCond %{REQUEST_URI} ^/wp-json/wp/v2/users [NC]
RewriteCond %{HTTP_COOKIE} !wordpress_logged_in
RewriteRule .* - [F,L]
# Block rest_route parameter enumeration
RewriteCond %{QUERY_STRING} rest_route=.*/users [NC]
RewriteCond %{HTTP_COOKIE} !wordpress_logged_in
RewriteRule .* - [F,L]
</IfModule>Application-level rate limiting (PHP transient-based):
<?php
/**
* Simple transient-based rate limiter for REST API requests.
* Limits each IP to 60 requests per minute on REST API routes.
* For high-traffic sites, replace transients with Redis or Memcached.
*/
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
$ip_address = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$transient = 'rest_rate_' . md5( $ip_address );
$max_requests = 60; // Requests per window
$window = 60; // Window in seconds
$current = (int) get_transient( $transient );
if ( $current >= $max_requests ) {
return new WP_Error(
'rest_rate_limited',
__( 'Rate limit exceeded. Try again shortly.' ),
array( 'status' => 429 )
);
}
// Increment counter; set expiry on first request in window
if ( 0 === $current ) {
set_transient( $transient, 1, $window );
} else {
set_transient( $transient, $current + 1, $window );
}
return $result;
}, 10, 3 );For production sites handling significant traffic, we recommend replacing transient-based limiting with a Redis-backed solution or a dedicated WAF like Cloudflare’s API Gateway.
4. Add CORS Headers to Lock Down Origins
Cross-Origin Resource Sharing (CORS) headers control which external domains can make requests to your REST API. WordPress does not set restrictive CORS headers by default, which means any website can make JavaScript requests to your API endpoints.
<?php
/**
* Restrict CORS to trusted origins only.
* Prevents cross-origin JavaScript requests from unauthorized domains.
* Add all domains that legitimately need API access.
*/
add_action( 'rest_api_init', function () {
// Remove WordPress default CORS headers
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
// Add restrictive CORS headers
add_filter( 'rest_pre_serve_request', function ( $served ) {
$allowed_origins = array(
'https://yourdomain.com',
'https://www.yourdomain.com',
'https://admin.yourdomain.com',
);
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ( in_array( $origin, $allowed_origins, true ) ) {
header( 'Access-Control-Allow-Origin: ' . $origin );
header( 'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS' );
header( 'Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce' );
header( 'Access-Control-Allow-Credentials: true' );
} else {
// Deny cross-origin requests from unknown domains
header( 'Access-Control-Allow-Origin: https://yourdomain.com' );
}
return $served;
});
}, 15 );Replace yourdomain.com with your actual domain. If you use a CDN or staging environment, add those origins to the $allowed_origins array. The X-WP-Nonce header is critical because WordPress uses nonce verification to authenticate cookie-based REST API requests from the admin interface.
5. Use Application Passwords or JWT for Headless Setups
If you run a headless WordPress site (WordPress as a back end with a separate JavaScript front end), you need a robust authentication strategy. WordPress 5.6 introduced application passwords as a core feature. For more complex setups, JSON Web Tokens (JWT) offer stateless authentication with expiration controls.
Application passwords are the simplest option. Generate one from the WordPress admin under Users > Profile > Application Passwords. Use it with Basic Authentication:
#!/bin/bash
# Authenticate REST API requests using application passwords
# The password is a space-separated token generated in WP Admin
API_USER="your-username"
APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" # From WP Admin
SITE_URL="https://yourdomain.com"
# Fetch private posts (requires authentication)
curl -s \
-u "${API_USER}:${APP_PASSWORD}" \
-H "Content-Type: application/json" \
"${SITE_URL}/wp-json/wp/v2/posts?status=draft" | jq '.[].title.rendered'JWT authentication requires a plugin such as JWT Authentication for WP REST API. Configure it by adding these constants to wp-config.php:
<?php
/**
* JWT Authentication configuration.
* Add to wp-config.php ABOVE the "That's all, stop editing!" line.
* Generate a strong secret key using: openssl rand -base64 64
*/
define( 'JWT_AUTH_SECRET_KEY', 'your-256-bit-secret-key-here' );
define( 'JWT_AUTH_CORS_ENABLE', false ); // We handle CORS ourselvesThen authenticate from your front end:
/**
* Authenticate with WordPress REST API using JWT.
* Store the token securely; never expose it in client-side code
* meant for public distribution.
*/
async function getJwtToken(username, password) {
const response = await fetch('https://yourdomain.com/wp-json/jwt-auth/v1/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error('Authentication failed');
}
const data = await response.json();
return data.token; // Use this token in Authorization: Bearer headers
}
/**
* Make an authenticated API request using the JWT token.
*/
async function fetchPrivatePosts(token) {
const response = await fetch('https://yourdomain.com/wp-json/wp/v2/posts?status=draft', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.json();
}For either method, always transmit credentials over HTTPS only. Application passwords are easier to manage and revoke. JWT tokens are better for stateless architectures where you need fine-grained expiration control.
6. Disable XML-RPC Alongside REST API Hardening
Hardening the REST API while leaving XML-RPC wide open is like locking your front door and leaving the garage open. XML-RPC’s system.multicall method allows attackers to test hundreds of username/password combinations in a single HTTP request, making brute force attacks vastly more efficient.
<?php
/**
* Completely disable XML-RPC.
* Blocks xmlrpc.php access and removes the pingback header.
* Safe for most sites; only keep XML-RPC if you use
* Jetpack, the WordPress mobile app or XML-RPC-dependent plugins.
*/
// Disable XML-RPC methods
add_filter( 'xmlrpc_enabled', '__return_false' );
// Remove the XML-RPC endpoint from headers
add_filter( 'wp_headers', function ( $headers ) {
unset( $headers['X-Pingback'] );
return $headers;
});
// Remove XML-RPC link from HTML head
remove_action( 'wp_head', 'rsd_link' );
remove_action( 'wp_head', 'wlwmanifest_link' );Pair this with an .htaccess rule to block direct access at the server level:
# Block all direct requests to xmlrpc.php
<Files "xmlrpc.php">
Require all denied
</Files>If you use Jetpack and need XML-RPC for its connection to WordPress.com, whitelist Jetpack’s IP ranges instead of leaving it fully open. Jetpack publishes their server IPs in their documentation.
7. Monitor and Log API Requests
You cannot secure what you do not observe. Logging REST API requests gives you visibility into reconnaissance attempts, credential stuffing patterns and endpoint abuse. This snippet logs all REST API requests to a custom database table.
<?php
/**
* Log all REST API requests to a custom database table.
* Creates the table on activation; logs method, route, IP and user agent.
* Review logs weekly or pipe them into a SIEM for real-time alerting.
*/
// Create logging table on theme/plugin activation
function qm_create_api_log_table() {
global $wpdb;
$table = $wpdb->prefix . 'rest_api_log';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
method VARCHAR(10) NOT NULL,
route VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent VARCHAR(500) DEFAULT '',
user_id BIGINT(20) UNSIGNED DEFAULT 0,
response_code SMALLINT(3) DEFAULT 0,
PRIMARY KEY (id),
INDEX idx_route (route),
INDEX idx_ip (ip_address),
INDEX idx_time (request_time)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
add_action( 'after_switch_theme', 'qm_create_api_log_table' );
// Log each REST API request
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
global $wpdb;
$table = $wpdb->prefix . 'rest_api_log';
$wpdb->insert( $table, array(
'method' => $request->get_method(),
'route' => $request->get_route(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0',
'user_agent' => substr( $_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500 ),
'user_id' => get_current_user_id(),
));
return $result;
}, 10, 3 );
// Optional: clean up logs older than 90 days via WP-Cron
add_action( 'wp_scheduled_delete', function () {
global $wpdb;
$table = $wpdb->prefix . 'rest_api_log';
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$table} WHERE request_time < %s",
gmdate( 'Y-m-d H:i:s', strtotime( '-90 days' ) )
)
);
});For enterprise deployments, pipe these logs into your SIEM (Splunk, Elastic or similar) using a custom REST endpoint or a wp-cli command. Set alerts for patterns like repeated 401 responses from a single IP, bulk requests to /wp/v2/users or unusual spikes in OPTIONS preflight requests.
WordPress REST API Security Checklist
Use this checklist during your next technical SEO and security audit. Each item maps to the hardening steps above.
| Check | Action | Priority |
|---|---|---|
| User enumeration blocked | Test /wp-json/wp/v2/users while logged out; should return 401 or empty | Critical |
| REST API requires authentication | Non-whitelisted endpoints return 401 for anonymous requests | Critical |
| Rate limiting active | Verify 429 response after exceeding threshold; test with ab or wrk | High |
| CORS restricted | Cross-origin requests from unauthorized domains are blocked | High |
| Application passwords or JWT configured | Headless setups use token-based auth with proper expiration | Medium (headless only) |
| XML-RPC disabled | xmlrpc.php returns 403; X-Pingback header removed | High |
| API logging enabled | All REST requests logged with IP, route and timestamp | High |
| REST API index restricted | Root /wp-json/ does not expose full route map to anonymous users | Medium |
| Nonce verification active | Cookie-based REST requests require valid X-WP-Nonce header | Critical |
| Content Security Policy set | CSP headers prevent inline script injection via API responses | Medium |
| Capability checks on custom endpoints | All custom register_rest_route calls include permission_callback | Critical |
| WordPress and plugins updated | Running latest stable versions; patch CVEs like CVE-2026-3506 promptly | Critical |
Frequently Asked Questions
Should I completely disable the WordPress REST API?
No. Disabling the REST API entirely breaks the Gutenberg block editor, many popular plugins (including Contact Form 7, WooCommerce and Yoast SEO services) and the WordPress mobile app. The correct approach is to restrict access: require authentication for sensitive endpoints, whitelist the public routes your site needs and block user enumeration. This preserves functionality while eliminating the attack surface.
How do I fix WordPress REST API user enumeration?
Add a rest_endpoints filter that removes /wp/v2/users and /wp/v2/users/(?P<id>[\d]+) for unauthenticated requests. Also block the legacy ?author=N enumeration vector with a template_redirect hook. Both code snippets are provided in Step 1 of this guide. After deployment, verify the fix by running curl -s https://yourdomain.com/wp-json/wp/v2/users from an unauthenticated session.
What is the difference between Application Passwords and JWT for REST API authentication?
Application passwords are built into WordPress core (since version 5.6) and work with HTTP Basic Authentication. They are simple to generate, easy to revoke from the admin dashboard and ideal for server-to-server integrations. JWT (JSON Web Tokens) are stateless tokens with built-in expiration. They require a plugin but offer better performance for headless front ends because the server does not need to query the database on every request. Use application passwords for simplicity; use JWT for stateless architectures with high request volumes.
Does disabling XML-RPC affect the WordPress REST API?
No. XML-RPC and the REST API are completely separate systems. XML-RPC uses xmlrpc.php with XML payloads; the REST API uses /wp-json/ with JSON. Disabling XML-RPC does not affect REST API functionality. We recommend disabling XML-RPC on every site that does not specifically require it because it exposes a parallel authentication endpoint that attackers exploit for brute force and DDoS amplification attacks.
How do I test if my WordPress REST API is properly secured?
Run these four tests from a terminal (not logged into WordPress). First: curl -s https://yourdomain.com/wp-json/wp/v2/users should return 401 or an empty result. Second: curl -s https://yourdomain.com/wp-json/ should not expose the full route index. Third: send 100 rapid requests to any endpoint and confirm you receive a 429 rate limit response. Fourth: make a cross-origin JavaScript fetch from a domain not in your CORS whitelist and verify the browser blocks it. Document the results in your security audit log.
Will REST API hardening affect my site's SEO or page speed?
REST API hardening has zero negative impact on SEO. Search engine crawlers (Googlebot, Bingbot) do not use the REST API to index your content; they crawl your rendered HTML pages. The hardening steps in this guide add negligible server overhead (under 1ms per request for the authentication check). Rate limiting can actually improve page speed under load by preventing API abuse from consuming server resources that your front-end visitors need.
What is the OWASP API Security Top 10 and how does it relate to WordPress?
The OWASP API Security Top 10 is a standardized list of the most critical API security risks. WordPress REST API hardening directly addresses several items on this list: Broken Object Level Authorization (restricting endpoint access), Broken Authentication (application passwords and JWT), Unrestricted Resource Consumption (rate limiting) and Security Misconfiguration (CORS headers and default endpoint exposure). Treating your WordPress REST API with the same rigor as any production API is the baseline for competent security practice.
When to Call in a Professional
The hardening steps above cover the fundamentals. If any of the following apply to your situation, bring in a specialist:
- You run a headless WordPress setup with a decoupled front end and need custom
permission_callbackfunctions with granular capability checks across dozens of custom endpoints. - Your site processes payments, health data or PII through REST API endpoints and needs to comply with PCI-DSS, HIPAA or PIPEDA.
- You have experienced a breach or active exploitation and need incident response, forensic analysis and remediation.
- Your hosting environment requires custom Nginx or Apache configurations that go beyond what managed hosting panels expose.
- You need to integrate WordPress REST API security with a broader Content Security Policy, WAF rules or a zero-trust network architecture.
At Quake Media, we build and maintain hardened WordPress sites for businesses across Vancouver and beyond. Our SEO services include technical security audits because rankings and security are inseparable: Google actively penalizes hacked sites and flags them in search results.
If your REST API is exposed right now, do not wait for an attacker to find it first.
Get a Free WordPress Security Audit
Our team will scan your REST API endpoints, check for user enumeration vulnerabilities and deliver a prioritized remediation plan. No obligation.
Call us: 604-901-7668
Request your free audit: quakemedia.ca/free-audit
Need help with this?
Quake Media helps businesses across Vancouver and Canada with SEO, PPC and custom web development. Get a free audit and see where your site stands.


