How to block /wp-json/batch/v1 without breaking your wp-admin

Every advisory says the same thing: block /wp-json/batch/v1. That advice is correct and incomplete. Here is the part that gets missed, and the fix that closes the hole without breaking your own admin.

Search the docs

⌘ K
Try "heartbeat", "restore from S3", or "WP-

Popular

License activation

WP-CLI install

Restore a backup

Hooks reference

Before anything else: update WordPress wp2shell (CVE-2026-63030, chained with CVE-2026-60137) is a critical unauthenticated remote code execution vulnerability in WordPress core, fixed in 6.9.5, 7.0.2, and later releases. If you're on 6.9.0 through 6.9.4 or 7.0.0 through 7.0.1, updating closes it completely and you can stop reading. Everything below is for sites you cannot update today, and attackers are actively exploiting this in the wild.

What you'll have when you finish

The batch endpoint attack path closed on a site you can't patch yet, either by requiring an authenticated editor on that one route (the approach that doesn't break anything) or by blocking it outright if that's all your host allows. Either way, you'll know how to verify it actually worked, and when to remove the stopgap.

  • Prerequisites: server or hosting access to add a must-use plugin, or edit .htaccess / nginx config. AdminEase (free) as an alternative if you'd rather not touch server files.
  • You'll need access to: WordPress admin, and either SFTP/file manager access or your AdminEase dashboard.
  • Time: about 10 minutes.
The walkthrough
  1. What the endpoint is for, and what breaks if you block it badly
  2. Both ways in: the URL form most rules miss
  3. The better approach: gate it, don't remove it
  4. If you'd rather block it outright
  5. Doing it from AdminEase instead
  6. Check that it worked

1. What the endpoint is for, and what breaks if you block it badly

/wp-json/batch/v1 landed in WordPress core so the block editor could send several REST requests in one round trip instead of firing them one at a time. It's infrastructure, not a feature anyone uses directly.

The most visible consumer is Appearance › Widgets on the block-based widgets screen. When you click Update there, the browser sends an OPTIONS preflight to /wp-json/batch/v1?_locale=user, then a POST with all the widget changes bundled together. The Site Editor uses the same mechanism when you save a template and a template part together.

Block it crudely and this is what you get Add a blanket 403 on batch/v1 at the web server and try to save a widget: you'll get a toast that says "The response is not a valid JSON response," with no mention of batching or the rule you just added. There's a Gutenberg issue confirming the screen doesn't fall back to individual REST calls when batch is unavailable. It was closed as not planned, so no fallback is coming. It just fails.

2. Both ways in: the URL form most rules miss

Before writing any rule, know that these two URLs reach the same endpoint:

https://example.com/wp-json/batch/v1 https://example.com/?rest_route=/batch/v1

The second form works even on sites using plain permalinks, and it's the one most copy-paste blocking rules miss. If you only block the pretty-permalink form, you haven't blocked anything. Any rule you write has to cover both.

3. The better approach: gate it, don't remove it

The vulnerability is unauthenticated: no account, no user interaction, nothing. That's the whole reason it scores as critical. Your own wp-admin, on the other hand, is always authenticated; the widgets screen calls batch/v1 as a logged-in editor with a valid nonce.

So instead of removing the endpoint, require a login on it. That closes the attack path and leaves your admin working exactly as before.

Create the file: add wp-content/mu-plugins/batch-guard.php with this content:
<?php /** * Require an authenticated editor for the REST batch endpoint. * Stopgap for CVE-2026-63030. Remove once WordPress is updated. */ add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) { if ( 0 !== strpos( $request->get_route(), '/batch/v1' ) ) { return $result; } if ( is_user_logged_in() && current_user_can( 'edit_posts' ) ) { return $result; } return new WP_Error( 'rest_forbidden', 'The batch endpoint requires an authenticated editor on this site.', array( 'status' => 401 ) ); }, 10, 3 );
Why a must-use plugin, not functions.php A mu-plugin loads before regular plugins, can't be switched off from the Plugins screen, and survives a theme change. For something standing between your site and a public exploit, that matters. rest_pre_dispatch runs after WordPress has worked out who's calling, so cookie authentication from wp-admin and application passwords both pass. Route matching catches both URL forms from step 2, since both resolve to the same internal route.

If some integration of yours calls batch as a lower-privileged user, loosen current_user_can( 'edit_posts' ) to a plain is_user_logged_in() check. Don't loosen it further than that.

4. If you'd rather block it outright

Some hosts won't let you add mu-plugins, and some people simply want the endpoint gone until they can patch. That's fine, but block both URL forms, and expect the widgets screen to break for you too (see the callout in step 1).

Nginx:
location ~* ^/wp-json/batch/v1 { return 403; } if ($arg_rest_route ~* "^/batch/v1") { return 403; }
Apache, in .htaccess above the WordPress rules:
<IfModule mod_rewrite.c> RewriteEngine On RewriteRule ^wp-json/batch/v1 - [F,L] RewriteCond %{QUERY_STRING} rest_route=/batch/v1 [NC] RewriteRule ^ - [F,L] </IfModule>

Put those rules above the # BEGIN WordPress block. Below it, the WordPress rewrite has already claimed the request.

5. Doing it from AdminEase instead

If you run AdminEase, there's no dedicated switch for the batch endpoint specifically, but the plugin's REST API control blocks anonymous requests across every REST route, which includes this one. It hooks into REST authentication rather than the URL, so both forms from step 2 are covered by the same setting, with no rewrite rules to get wrong.

Open the setting: WordPress Admin › AdminEase › Security › Hardening, then turn on Disable REST API.
Keep your admin working: immediately enable the child setting Enable for logged-in users. Without it, Gutenberg, the widgets screen, and any page builder that relies on the REST API will start failing for you too, not just for attackers.

One difference from step 3 worth knowing: Enable for logged-in users admits any logged-in user, not only editors. On a site with open registration or WooCommerce customer accounts, that is a looser gate than the current_user_can( 'edit_posts' ) check in the mu-plugin. It still closes the unauthenticated path, which is the one that matters here.

This is broader than the mu-plugin snippet Turning this on blocks all anonymous REST API traffic, not just batch, which closes off /wp-json/batch/v1 along with unrelated things like anonymous user enumeration. Check it against your own site first: any integration that reads your REST API without logging in (a headless frontend, a webhook, an external service pulling posts) will be affected. For most standard WordPress sites, nothing outside wp-admin touches the REST API anonymously, and this is the shortest path to the same protection. AdminEase Pro adds allowlist fields (by IP or by specific route) under the same setting, for cases where something legitimate does need anonymous access.

6. Check that it worked

Test both URL forms while logged out: sign out, or open a private window, and run:
curl -i https://example.com/wp-json/batch/v1 curl -i "https://example.com/?rest_route=/batch/v1"

Both should return 401 or 403. If either returns 404 with a JSON body, or anything that looks like a normal REST response, that rule isn't catching that form yet.

Test your own admin while logged in: sign back in and save something on Appearance › Widgets.

If it saves, you're done. If you get "The response is not a valid JSON response," your rule is blocking authenticated requests too, and you want the gated approach from step 3 rather than an outright block.

This is a stopgap, not a hardening measure Once the site is on 6.9.5, 7.0.2, or later, remove the mu-plugin or the server rule. Leaving a permanent block on a core endpoint is the kind of thing that breaks a future WordPress release long after everyone has forgotten why it's there. Put the reason and a removal note in a comment at the top of the file, the way the snippet in step 3 does.

Where to go from here

The batch endpoint is gated (or blocked) on a site you couldn't patch immediately, and you've confirmed your own admin still works. Treat this as time bought, not a fix: schedule the WordPress core update to 6.9.5, 7.0.2, or later, and once it's done, remove whichever stopgap you added here.

Frequently asked questions

Does blocking this fix the vulnerability?

No. It closes one route to it. Updating WordPress fixes it. Treat blocking as something you do for the hours or days until you can update, not instead of updating.

Am I affected?

The full remote code execution chain affects WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1. The SQL injection half of the chain (CVE-2026-60137) reaches back to 6.8.0 through 6.8.5, so those versions need updating too even though the code execution path doesn't apply to them. Check Dashboard › Updates, or Tools › Site Health › Info › WordPress.

Will this break my page builder or my forms plugin?

The gated mu-plugin from step 3 only touches /batch/v1, and only for requests with no authenticated editor behind them; front-end form submissions don't use it. AdminEase's broader "Disable REST API" setting is different: it affects anything reading your REST API anonymously, so test before you rely on it if you run a headless frontend or similar integration.

What about "disable-json-api" style plugins?

Most of those block wp/v2 routes and leave batch/v1 alone, because it isn't a content endpoint. Verify with the curl check in step 6 rather than assuming.

Does blocking it hurt performance?

No. The endpoint exists to save round trips in the editor. Blocking it costs a handful of extra requests when saving widgets, and nothing at all on the front end.

How do I tell whether anything on my site actually uses it?

Watch your access log for requests to /wp-json/batch/v1 and for query strings containing rest_route=/batch/v1 over a normal week of editing. On most sites the only hits are your own admin sessions. While you're in there, read the status codes as well: a 207 Multi-Status response to a batch request from an address that isn't yours is the signature of an exploitation attempt that went through, and is worth treating as a possible compromise rather than as noise.

Can I block just the batch endpoint with AdminEase and leave the rest of the REST API public?

No. AdminEase's REST API control is all-or-nothing for anonymous requests, with allowlist exceptions (Pro) by IP or route. For a narrower, batch-only block, use the mu-plugin from step 3 instead.

Related reading

Keep going: Hardening WordPress security · Block countries in WordPress for free · Geo-blocking and bot protection

This walkthrough uses AdminEase.

Was this article helpful?

On this page
Back to top
Copy link to article