Skip to content

Instantly share code, notes, and snippets.

@szepeviktor
Last active July 25, 2026 00:46
Show Gist options
  • Select an option

  • Save szepeviktor/a6e665c6303e176d13bc8fd15f2da19c to your computer and use it in GitHub Desktop.

Select an option

Save szepeviktor/a6e665c6303e176d13bc8fd15f2da19c to your computer and use it in GitHub Desktop.
Temporary mitigation for chained RCE in WordPress core: CVE-2026-60137 and CVE-2026-63030
<?php
/*
* Plugin Name: Fix REST Batch
* Description: Rejects malformed REST batch paths before affected WordPress core versions process them.
*/
declare(strict_types=1);
namespace SzepeViktor\FixRestBatch;
final class REST_Server extends \WP_REST_Server
{
/**
* Reject malformed batch paths before delegating valid requests to WordPress core.
*
* @param \WP_REST_Request $batch_request The batch request.
* @return \WP_REST_Response|\WP_Error
*/
public function serve_batch_request_v1(\WP_REST_Request $batch_request)
{
$requests = $batch_request['requests'];
if (!\is_array($requests)) {
return new \WP_Error(
'rest_invalid_batch',
\__('The batch requests parameter must be an array.'),
['status' => 400]
);
}
foreach ($requests as $args) {
if (
!\is_array($args)
|| !isset($args['path'])
|| !\is_string($args['path'])
|| false === \wp_parse_url($args['path'])
) {
return new \WP_Error(
'parse_path_failed',
\__('Could not parse the path.'),
['status' => 400]
);
}
}
return parent::serve_batch_request_v1($batch_request);
}
}
final class Plugin
{
/**
* Register the REST server replacement before WordPress creates the server instance.
*/
public static function boot(): void
{
\add_filter(
'wp_rest_server_class',
[self::class, 'filter_rest_server_class'],
PHP_INT_MAX
);
}
/**
* Use the patched server only while core lacks safe REST batch error handling.
*
* @param string $class_name Current REST server class name.
* @return string
*/
public static function filter_rest_server_class($class_name)
{
global $wp_version;
$core_requires_fix = \version_compare($wp_version, '5.6', '>=')
&& (
\version_compare($wp_version, '6.8.6', '<=')
|| (
\version_compare($wp_version, '6.9-alpha', '>=')
&& \version_compare($wp_version, '6.9.5', '<')
)
|| (
\version_compare($wp_version, '7.0-alpha', '>=')
&& \version_compare($wp_version, '7.0.2', '<')
)
|| (
\version_compare($wp_version, '7.1-alpha', '>=')
&& \version_compare($wp_version, '7.1-beta2', '<')
)
);
if (!$core_requires_fix) {
return $class_name;
}
if (\WP_REST_Server::class !== $class_name) {
\error_log(
'Fix REST Batch did not replace the REST server because another custom server class is active.'
);
return $class_name;
}
return REST_Server::class;
}
}
Plugin::boot();
<?php
/*
* Plugin Name: Emergency Security Guard
* Description: Temporary mitigation for chained RCE: CVE-2026-60137 and CVE-2026-63030
*/
/**
* Sanitize author exclusions before WP_Query builds SQL.
*/
function emergency_wp_sanitize_author_not_in($query)
{
$value = $query->get('author__not_in');
if ($value === null || $value === '' || $value === []) {
return;
}
if (is_int($value)) {
$value = [$value];
}
if (!is_array($value) && !is_string($value)) {
$query->set('author__not_in', []);
return;
}
$ids = array_values(array_filter(wp_parse_id_list($value)));
$query->set('author__not_in', $ids);
}
add_action('parse_query', 'emergency_wp_sanitize_author_not_in', PHP_INT_MAX);
/**
* Disable the REST batch endpoint.
*/
function emergency_wp_block_rest_batch($result, $server, $request)
{
if (untrailingslashit($request->get_route()) === '/batch/v1') {
return new WP_Error(
'emergency_batch_disabled',
'REST batch requests are temporarily disabled.',
['status' => 403]
);
}
return $result;
}
add_filter('rest_pre_dispatch', 'emergency_wp_block_rest_batch', -9999, 3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment