diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php old mode 100644 new mode 100755 index 2a663e11e97e3..a25dae81decc9 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -104,7 +104,8 @@ class WP_Http { * Send an HTTP request to a URI. * * Please note: The only URI that are supported in the HTTP Transport implementation - * are the HTTP and HTTPS protocols. + * are the HTTP and HTTPS protocols. The `webcal` and `webcals` schemes are normalized + * to HTTPS via wp_http_normalize_url() before the request is sent. * * @since 2.7.0 * @@ -280,12 +281,30 @@ public function request( $url, $args = array() ) { return $pre; } + $url = wp_http_normalize_url( $url ); + if ( function_exists( 'wp_kses_bad_protocol' ) ) { if ( $parsed_args['reject_unsafe_urls'] ) { $url = wp_http_validate_url( $url ); } if ( $url ) { - $url = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) ); + /** + * Controls the list of URL protocols allowed in HTTP API requests. + * + * Warning: Only `http` and `https` are supported by HTTP transports. Allowing + * other protocols increases SSRF risk unless handled via the {@see 'pre_http_request'} + * filter or custom transport logic. + * + * @since 6.9.0 + * + * @param string[] $protocols Array of allowed URL protocols. + * @param string $url Requested URL. + */ + $allowed_protocols = apply_filters( 'http_allowed_protocols', array( 'http', 'https', 'ssl' ), $url ); + if ( ! is_array( $allowed_protocols ) ) { + $allowed_protocols = array( 'http', 'https', 'ssl' ); + } + $url = wp_kses_bad_protocol( $url, $allowed_protocols ); } } diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php old mode 100644 new mode 100755 index 8280f424934dd..f012886ec7e63 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -62,13 +62,15 @@ function wp_safe_remote_request( $url, $args = array() ) { * URL. The URL, and every URL it redirects to, are validated with wp_http_validate_url() * to avoid Server Side Request Forgery attacks (SSRF). * - * The only supported protocols are `http` and `https`. + * The only supported protocols are `http` and `https`. Calendar `webcal` and `webcals` + * URLs are normalized to `https` before the request is sent. * * @since 3.6.0 * * @see wp_remote_request() For more information on the response array format. * @see WP_Http::request() For default arguments information. * @see wp_http_validate_url() For more information about how the URL is validated. + * @see wp_http_normalize_url() For calendar URL normalization. * * @link https://owasp.org/www-community/attacks/Server_Side_Request_Forgery * @@ -173,10 +175,13 @@ function wp_remote_request( $url, $args = array() ) { * * Important: If the URL is user-controlled, use `wp_safe_remote_get()` instead. * + * Calendar `webcal` and `webcals` URLs are normalized to `https` before the request is sent. + * * @since 2.7.0 * * @see wp_remote_request() For more information on the response array format. * @see WP_Http::request() For default arguments information. + * @see wp_http_normalize_url() For calendar URL normalization. * * @param string $url URL to retrieve. * @param array $args Optional. Request arguments. Default empty array. @@ -532,10 +537,46 @@ function send_origin_headers() { return false; } +/** + * Normalizes a URL for use with the HTTP API transport layer. + * + * Calendar subscription schemes `webcal` and `webcals` are rewritten to `https` + * because HTTP transports only support `http` and `https`. + * + * @since 6.9.0 + * + * @param string $url Request URL. + * @return string Normalized URL. + */ +function wp_http_normalize_url( $url ) { + if ( ! is_string( $url ) || '' === $url ) { + return $url; + } + + $original_url = $url; + + if ( preg_match( '#^webcal:#i', $url ) ) { + $url = preg_replace( '#^webcal:#i', 'https:', $url ); + } elseif ( preg_match( '#^webcals:#i', $url ) ) { + $url = preg_replace( '#^webcals:#i', 'https:', $url ); + } + + /** + * Filters the normalized HTTP request URL. + * + * @since 6.9.0 + * + * @param string $url Normalized URL. + * @param string $original_url Original URL before normalization. + */ + return apply_filters( 'http_normalize_url', $url, $original_url ); +} + /** * Validates a URL as safe for use in the HTTP API. * - * The only supported protocols are `http` and `https`. + * The only supported protocols are `http` and `https`. The `webcal` and `webcals` + * schemes are accepted and validated after normalization to `https`. * * Examples of URLs that are considered unsafe: * @@ -561,13 +602,32 @@ function wp_http_validate_url( $url ) { return false; } - $original_url = $url; - $url = wp_kses_bad_protocol( $url, array( 'http', 'https' ) ); - if ( ! $url || strtolower( $url ) !== strtolower( $original_url ) ) { + $original_url = $url; + $validation_url = wp_http_normalize_url( $url ); + + /** + * Controls the list of URL protocols allowed in HTTP API validation. + * + * Warning: Only `http` and `https` are supported by HTTP transports. Allowing + * other protocols increases SSRF risk unless handled via the {@see 'pre_http_request'} + * filter or custom transport logic. + * + * @since 6.9.0 + * + * @param string[] $protocols Array of allowed URL protocols. + * @param string $url Requested URL. + */ + $allowed_protocols = apply_filters( 'http_allowed_protocols', array( 'http', 'https' ), $original_url ); + if ( ! is_array( $allowed_protocols ) ) { + $allowed_protocols = array( 'http', 'https' ); + } + + $url = wp_kses_bad_protocol( $validation_url, $allowed_protocols ); + if ( ! $url || strtolower( $url ) !== strtolower( $validation_url ) ) { return false; } - $parsed_url = parse_url( $url ); + $parsed_url = parse_url( $validation_url ); if ( ! $parsed_url || empty( $parsed_url['host'] ) ) { return false; } @@ -611,7 +671,7 @@ function wp_http_validate_url( $url ) { * @param string $host Host name of the requested URL. * @param string $url Requested URL. */ - if ( ! apply_filters( 'http_request_host_is_external', false, $host, $url ) ) { + if ( ! apply_filters( 'http_request_host_is_external', false, $host, $validation_url ) ) { return false; } } @@ -619,7 +679,7 @@ function wp_http_validate_url( $url ) { } if ( empty( $parsed_url['port'] ) ) { - return $url; + return $original_url; } $port = $parsed_url['port']; @@ -636,13 +696,13 @@ function wp_http_validate_url( $url ) { * @param string $host Host name of the requested URL. * @param string $url Requested URL. */ - $allowed_ports = apply_filters( 'http_allowed_safe_ports', array( 80, 443, 8080 ), $host, $url ); + $allowed_ports = apply_filters( 'http_allowed_safe_ports', array( 80, 443, 8080 ), $host, $validation_url ); if ( is_array( $allowed_ports ) && in_array( $port, $allowed_ports, true ) ) { - return $url; + return $original_url; } if ( $parsed_home && $same_host && isset( $parsed_home['port'] ) && $parsed_home['port'] === $port ) { - return $url; + return $original_url; } return false; diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php old mode 100644 new mode 100755 index 651064dc5674c..49921fe0f364d --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -450,6 +450,9 @@ public function data_wp_http_validate_url_should_validate() { 'url' => 'https://example.com:81/caniload.php', 'cb_safe_ports' => 'callback_custom_safe_ports', ), + 'a webcal url' => array( + 'url' => 'webcal://example.com/caniload.php', + ), ); } @@ -577,6 +580,103 @@ public function callback_remove_safe_ports( $ports ) { return array(); } + /** + * @ticket 49385 + * + * @covers ::wp_http_normalize_url + */ + public function test_wp_http_normalize_url_rewrites_webcal_to_https() { + $this->assertSame( + 'https://example.com/feed.ics', + wp_http_normalize_url( 'webcal://example.com/feed.ics' ) + ); + $this->assertSame( + 'https://example.com/feed.ics', + wp_http_normalize_url( 'WEBCAL://example.com/feed.ics' ) + ); + $this->assertSame( + 'https://example.com/feed.ics', + wp_http_normalize_url( 'webcals://example.com/feed.ics' ) + ); + } + + /** + * @ticket 49385 + * + * @covers ::wp_http_validate_url + */ + public function test_wp_http_validate_url_accepts_webcal_url() { + $url = 'webcal://example.com/caniload.php'; + $this->assertSame( $url, wp_http_validate_url( $url ) ); + } + + /** + * @ticket 49385 + * + * @covers ::wp_http_validate_url + */ + public function test_wp_http_validate_url_still_rejects_ftp() { + $this->assertFalse( wp_http_validate_url( 'ftp://example.com/caniload.php' ) ); + } + + /** + * @ticket 49385 + * + * @covers ::wp_http_validate_url + */ + public function test_http_allowed_protocols_filter_runs_in_validate_url() { + $filter_ran = false; + + add_filter( + 'http_allowed_protocols', + function ( $protocols, $url ) use ( &$filter_ran ) { + $filter_ran = true; + $this->assertSame( 'http://example.com/caniload.php', $url ); + return $protocols; + }, + 10, + 2 + ); + + wp_http_validate_url( 'http://example.com/caniload.php' ); + + $this->assertTrue( $filter_ran ); + } + + /** + * @ticket 49385 + * + * @covers ::wp_remote_get + * @covers ::wp_http_normalize_url + */ + public function test_wp_remote_get_normalizes_webcal_url_before_request() { + $request_url = null; + + add_filter( + 'pre_http_request', + function ( $response, $parsed_args, $url ) use ( &$request_url ) { + $request_url = $url; + return array( + 'headers' => array(), + 'body' => '', + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + }, + 10, + 3 + ); + + $result = wp_remote_get( 'webcal://example.com/feed.ics' ); + + $this->assertNotWPError( $result ); + $this->assertSame( 'https://example.com/feed.ics', $request_url ); + } + /** * Test HTTP Redirects with multiple Location headers specified. *