The WordPress REST API exposes dozens of routes by default — many of which your theme or plugin never uses. Disabling unused endpoints reduces the attack surface, hides version information, and prevents automated scanners from enumerating your content structure.
Problem: The WordPress REST API exposes all registered routes by default — including routes from inactive or development plugins — increasing the attack surface and making it easier for attackers to probe available endpoints.
Solution: Disable unused REST routes selectively using the rest_endpoints filter — remove specific routes by unsetting their keys from the endpoints array. Restrict sensitive routes to authenticated users with the permission_callback argument. Add Nginx rate limiting on /wp-json/ to limit unauthenticated probing.
The examples below disable the REST API for unauthenticated users, remove specific default routes that expose sensitive data, restrict the users endpoint, and add authentication requirements to custom routes.
401 ]
);
}
return $result;
} );
// 2. Allow specific public endpoints to bypass the above restriction
// Useful if you have a headless front end or contact form using the REST API
add_filter( 'rest_authentication_errors', function( $result ) {
$request_path = parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
// Allow public access to these routes
$allowed_public = [
'/wp-json/contact-form-7/v1/contact-forms',
'/wp-json/myplugin/v1/products',
];
foreach ( $allowed_public as $path ) {
if ( str_contains( $request_path, $path ) ) {
return $result; // no auth required
}
}
if ( ! empty( $result ) ) return $result;
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'Authentication required.', [ 'status' => 401 ] );
}
return $result;
}, 20 );
// 3. Remove the /wp-json/wp/v2/users endpoint (exposes usernames)
add_filter( 'rest_endpoints', function( array $endpoints ): array {
// Remove public user enumeration
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
// Remove oEmbed discovery
unset( $endpoints['/oembed/1.0'] );
unset( $endpoints['/oembed/1.0/embed'] );
unset( $endpoints['/oembed/1.0/proxy'] );
return $endpoints;
} );
Remove REST API version discovery headers and links:
(hides /wp-json/ discovery)
remove_action( 'wp_head', 'rest_output_link_wp_head' );
remove_action( 'template_redirect', 'rest_output_link_header', 11 );
remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' );
// 5. Remove the X-WP-Total and X-WP-TotalPages headers (leaks content counts)
add_filter( 'rest_post_query', function( array $args, WP_REST_Request $request ): array {
// Set no_found_rows=true to skip the COUNT(*) query and suppress headers
if ( ! current_user_can( 'manage_options' ) ) {
$args['no_found_rows'] = true;
}
return $args;
}, 10, 2 );
// 6. Restrict a custom route to admins only
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/admin-report', [
'methods' => 'GET',
'callback' => 'myplugin_admin_report_handler',
'permission_callback' => function(): bool {
return current_user_can( 'manage_options' );
},
] );
} );
// 7. Rate-limit REST API requests per IP using transients
add_filter( 'rest_pre_dispatch', function( $result, $server, WP_REST_Request $request ) {
$ip = sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? '' );
$key = 'rest_rate_' . md5( $ip );
$count = (int) get_transient( $key );
if ( $count >= 60 ) { // max 60 requests/minute per IP
return new WP_Error( 'rest_rate_limited', 'Too many requests.', [ 'status' => 429 ] );
}
set_transient( $key, $count + 1, 60 );
return $result;
}, 10, 3 );
NOTE: Never disable the REST API entirely with a blanket rest_enabled filter — the Gutenberg editor, WooCommerce admin, and many plugins depend on it. Instead, require authentication for non-whitelisted routes, remove the specific endpoints that expose sensitive data (users, oEmbed), and enforce permission callbacks on every custom route.