diff --git a/RELEASE-NOTES-1.35 b/RELEASE-NOTES-1.35 index c6f567f553f..5e223a60811 100644 --- a/RELEASE-NOTES-1.35 +++ b/RELEASE-NOTES-1.35 @@ -60,6 +60,8 @@ For notes on 1.34.x and older releases, see HISTORY. page edit. Only has effect if $wgWatchlistExpiry is true. * $wgImgAuthPath can be used to override the path prefix used when handling img_auth.php requests. (T235357) +* $wgAllowedCorsHeaders — list of headers which can be used in a cross-site API + request. * … ==== Changed configuration ==== diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0f35008cac1..84311a67ad5 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8567,6 +8567,24 @@ $wgCrossSiteAJAXdomains = []; */ $wgCrossSiteAJAXdomainExceptions = []; +/** + * List of allowed headers for cross-origin API requests. + */ +$wgAllowedCorsHeaders = [ + /* simple headers (see spec) */ + 'Accept', + 'Accept-Language', + 'Content-Language', + 'Content-Type', + /* non-authorable headers in XHR, which are however requested by some UAs */ + 'Accept-Encoding', + 'DNT', + 'Origin', + /* MediaWiki whitelist */ + 'User-Agent', + 'Api-User-Agent', +]; + /** * Enable the experimental REST API. * diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 84434a4a24d..3e2cd5efaaf 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -737,8 +737,9 @@ class ApiMain extends ApiBase { } // We allow the actual request to send the following headers $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' ); + $allowedHeaders = $this->getConfig()->get( 'AllowedCorsHeaders' ); if ( $requestedHeaders !== false ) { - if ( !self::matchRequestedHeaders( $requestedHeaders ) ) { + if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) { $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' ); return true; } @@ -809,30 +810,18 @@ class ApiMain extends ApiBase { * of headers that we allow the follow up request to send. * * @param string $requestedHeaders Comma separated list of HTTP headers + * @param string[] $allowedHeaders List of allowed HTTP headers * @return bool True if all requested headers are in the list of allowed headers */ - protected static function matchRequestedHeaders( $requestedHeaders ) { + protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) { if ( trim( $requestedHeaders ) === '' ) { return true; } $requestedHeaders = explode( ',', $requestedHeaders ); - $allowedAuthorHeaders = array_flip( [ - /* simple headers (see spec) */ - 'accept', - 'accept-language', - 'content-language', - 'content-type', - /* non-authorable headers in XHR, which are however requested by some UAs */ - 'accept-encoding', - 'dnt', - 'origin', - /* MediaWiki whitelist */ - 'user-agent', - 'api-user-agent', - ] ); + $allowedHeaders = array_change_key_case( array_flip( $allowedHeaders ), CASE_LOWER ); foreach ( $requestedHeaders as $rHeader ) { $rHeader = strtolower( trim( $rHeader ) ); - if ( !isset( $allowedAuthorHeaders[$rHeader] ) ) { + if ( !isset( $allowedHeaders[$rHeader] ) ) { LoggerFactory::getInstance( 'api-warning' )->warning( 'CORS preflight failed on requested header: {header}', [ 'header' => $rHeader diff --git a/tests/phpunit/includes/api/ApiMainTest.php b/tests/phpunit/includes/api/ApiMainTest.php index ffc0f14d2ea..4c4da35f918 100644 --- a/tests/phpunit/includes/api/ApiMainTest.php +++ b/tests/phpunit/includes/api/ApiMainTest.php @@ -1153,4 +1153,15 @@ class ApiMainTest extends ApiTestCase { $this->assertArrayHasKey( 'code', $data['error'] ); $this->assertSame( 'badvalue', $data['error']['code'] ); } + + public function testMatchRequestedHeaders() { + $api = Wikimedia\TestingAccessWrapper::newFromClass( 'ApiMain' ); + $allowedHeaders = [ 'Accept', 'Origin', 'User-Agent' ]; + + $this->assertTrue( $api->matchRequestedHeaders( 'Accept', $allowedHeaders ) ); + $this->assertTrue( $api->matchRequestedHeaders( 'Accept,Origin', $allowedHeaders ) ); + $this->assertTrue( $api->matchRequestedHeaders( 'accEpt, oRIGIN', $allowedHeaders ) ); + $this->assertFalse( $api->matchRequestedHeaders( 'Accept,Foo', $allowedHeaders ) ); + $this->assertFalse( $api->matchRequestedHeaders( 'Accept, fOO', $allowedHeaders ) ); + } }