Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3f08a51
fix: tests and conditional set of defaults
moonmeister Aug 17, 2026
694fccd
REST API: Add the `rest_allowed_cors_methods` filter.
moonmeister Aug 18, 2026
8bb0c27
REST API: Trim the ordering comment in `rest_send_cors_headers()`.
moonmeister Aug 18, 2026
d8de848
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
moonmeister Aug 18, 2026
91580d9
REST API: Let `rest_allowed_cors_methods` override a response's metho…
moonmeister Sep 15, 2026
353f3ba
Merge branch 'fix/rest-cors-allow-methods-clobber' of github.com:moon…
moonmeister Sep 15, 2026
4f7a663
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
moonmeister Sep 15, 2026
85c3d9b
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
adamsilverstein Sep 30, 2026
c1f3194
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
adamsilverstein Oct 17, 2026
696734b
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
adamsilverstein Oct 19, 2026
d2c2f0e
Add missing ext-xdebug to suggest in composer.json
westonruter Oct 19, 2026
86857b2
Fix PHPStan errors in tests
westonruter Oct 19, 2026
326b89e
Add native type hints for params
westonruter Oct 19, 2026
f961c3e
Apply batched suggestions from code review
adamsilverstein Oct 19, 2026
f1de1db
Fix syntax error from prior suggestion
westonruter Oct 19, 2026
f862087
REST API: Fix the parse error in rest_send_cors_headers().
adamsilverstein Oct 19, 2026
9a017d9
Tests: Drop the Xdebug dependency from the REST CORS header tests.
adamsilverstein Oct 19, 2026
62058a0
REST API: Harden the arguments to rest_send_cors_headers().
adamsilverstein Oct 19, 2026
674e883
REST API: Remove the CORS methods header when the filtered list is em…
adamsilverstein Oct 19, 2026
3756a7f
REST API: Validate CORS methods from the response by syntax, not the …
moonmeister Oct 19, 2026
a8fc56d
Merge branch 'trunk' into fix/rest-cors-allow-methods-clobber
moonmeister Oct 19, 2026
55010b8
REST API: Send CORS headers through WP_REST_Server instead of raw hea…
moonmeister Oct 20, 2026
ca6d58d
Merge branch 'fix/rest-cors-allow-methods-clobber' of github.com:moon…
moonmeister Oct 20, 2026
fab020a
Fix PHPStan errors
westonruter Oct 20, 2026
f5c14f6
Fix PHPStan errors in tests
westonruter Oct 20, 2026
e8ecdd9
Add a since tag
westonruter Oct 20, 2026
96b7592
REST API: Keep the WP_REST_Server::send_header() signature unchanged.
westonruter Oct 20, 2026
2614921
REST API: Drop the native parameter types from rest_send_cors_headers().
westonruter Oct 20, 2026
2a02e41
REST API: Avoid setting up the REST server from rest_send_cors_header…
westonruter Oct 20, 2026
a557b2e
REST API: Validate the filtered CORS method list.
westonruter Oct 20, 2026
9c01533
REST API: Fall back to the default CORS methods for an unusable respo…
westonruter Oct 20, 2026
9359781
Docs: Note the `$server` parameter of rest_send_cors_headers() in its…
westonruter Oct 20, 2026
fc72d81
Add phpdoc to address PHPStan error for closure return value
westonruter Oct 20, 2026
382df14
REST API: Reject CORS methods with a trailing newline.
westonruter Oct 20, 2026
24664ea
REST API: Drop CORS method entries that are not strings.
westonruter Oct 20, 2026
c40d5dd
REST API: Split filtered CORS method entries like a string value.
westonruter Oct 20, 2026
68bfacd
REST API: Remove the CORS methods header for an empty list without a …
westonruter Oct 20, 2026
06f2f2c
Tests: Describe how the REST CORS header tests observe headers.
westonruter Oct 20, 2026
a33ad8f
Tests: Use the default CORS methods constant in the assertions.
westonruter Oct 20, 2026
2715e83
Tests: Restore the request method after the REST CORS header tests.
westonruter Oct 20, 2026
852f1f0
REST API: Ignore a filtered CORS method value that is not a list.
westonruter Oct 20, 2026
08385b0
REST API: Send each CORS method only once.
westonruter Oct 20, 2026
8b844f8
Tests: Restore the Origin after the REST CORS header tests.
westonruter Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 94 additions & 7 deletions src/wp-includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ function rest_api_default_filters() {
}

// Default serving.
add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers', 10, 4 );
add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );

Expand Down Expand Up @@ -808,22 +808,109 @@ function rest_handle_doing_it_wrong( $function_name, $message, $version ) {
* Sends Cross-Origin Resource Sharing headers with API requests.
*
* @since 4.4.0
* @since 7.2.0 Added the `$result`, `$request`, and `$server` parameters and the `rest_allowed_cors_methods` filter.
*
* @param mixed $value Response data.
* @param mixed $value Response data.
* @param WP_HTTP_Response|null $result Optional. Result to send to the client. Default null.
* @param WP_REST_Request|null $request Optional. The request in context. Default null.
* @param WP_REST_Server|null $server Optional. The server instance. Default null.
* @return mixed Response data.
*/
function rest_send_cors_headers( $value ) {
function rest_send_cors_headers( $value, $result = null, $request = null, $server = null ) {
$origin = get_http_origin();

if ( $origin ) {
// Requests from file:// and data: URLs send "Origin: null".
if ( 'null' !== $origin ) {
$origin = sanitize_url( $origin );
}
header( 'Access-Control-Allow-Origin: ' . $origin );
header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
header( 'Access-Control-Allow-Credentials: true' );
header( 'Vary: Origin', false );

/*
* Send through the server when serving a REST request. A direct call without one
* sends the headers itself, so that it does not set up the REST server.
*/
if ( ! $server instanceof WP_REST_Server ) {
$server = null;
}
$send_header = static function ( string $key, string $header_value ) use ( $server ): void {
if ( $server ) {
$server->send_header( $key, $header_value );
} elseif ( ! headers_sent() ) {
header( "{$key}: {$header_value}" );
}
};

$send_header( 'Access-Control-Allow-Origin', $origin );

/*
* The response's own headers have already been sent, and header() replaces them,
* so seed the list from any value the response set rather than discarding it.
*/
$response_headers = ( $result instanceof WP_HTTP_Response ) ? $result->get_headers() : array();
$response_headers = array_change_key_case( $response_headers );

/**
* Parses a list of methods, keeping only syntactically valid method tokens (RFC 9110, section 9.1).
*
* @phpstan-var Closure(mixed): list<non-empty-string> $parse_methods
*/
$parse_methods = static function ( $methods ): array {
if ( is_array( $methods ) ) {
// Join the entries so that each one is split like a string value, as in array( 'GET, POST' ).
$methods = implode( ',', array_filter( $methods, 'is_string' ) );
} elseif ( ! is_string( $methods ) ) {
return array();
}
$methods = preg_grep( '/^[!#$%&\'*+.^_`|~0-9A-Za-z-]+$/D', array_unique( wp_parse_list( $methods ) ) );
return is_array( $methods ) ? array_values( $methods ) : array();
};

// Fall back to the default list when the response names no valid methods.
$allow_methods = $parse_methods( $response_headers['access-control-allow-methods'] ?? null );
if ( empty( $allow_methods ) ) {
$allow_methods = array( 'OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' );
}

/**
* Filters the list of HTTP methods advertised to REST API CORS requests.
*
* The allowed methods are passed to the browser to specify which methods
* may be used in a cross-origin request to the REST API. The list is a
* fixed default and is not derived from the registered routes, so a site
* serving routes that accept other methods can advertise them here.
*
* If the response already set its own `Access-Control-Allow-Methods` header
* naming valid methods, `$allow_methods` is those methods instead of the default list.
* Entries that are not valid method tokens are dropped, and an empty list
* omits the header. A value that is neither an array nor a string is ignored.
*
* @since 7.2.0
*
* @param string[] $allow_methods The list of HTTP methods to allow.
* @param WP_REST_Request|null $request The request in context, if available.
*/
$filtered_methods = apply_filters( 'rest_allowed_cors_methods', $allow_methods, $request instanceof WP_REST_Request ? $request : null );

// Keep the unfiltered list for a callback that does not return one, rather than omitting the header.
if ( is_array( $filtered_methods ) || is_string( $filtered_methods ) ) {
$allow_methods = $parse_methods( $filtered_methods );
}

if ( ! empty( $allow_methods ) ) {
$send_header( 'Access-Control-Allow-Methods', implode( ', ', $allow_methods ) );
} elseif ( $server ) {
// Remove a value the response already sent.
$server->remove_header( 'Access-Control-Allow-Methods' );
} elseif ( ! headers_sent() ) {
header_remove( 'Access-Control-Allow-Methods' );
}
Comment thread
moonmeister marked this conversation as resolved.

$send_header( 'Access-Control-Allow-Credentials', 'true' );

// WP_REST_Server::send_header() replaces a header, and Vary must be appended to.
if ( ! headers_sent() ) {
header( 'Vary: Origin', false );
}
} elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) {
header( 'Vary: Origin', false );
}
Expand Down
1 change: 1 addition & 0 deletions tests/phpunit/includes/spy-rest-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Spy_REST_Server extends WP_REST_Server {

/** @var array<string, string> */
public $sent_headers = array();
public $sent_body = '';
public $last_request = null;
Expand Down
Loading
Loading