From 96ee2673430f36a91c04541e6a7afe9827a5dd6e Mon Sep 17 00:00:00 2001 From: Andrew Nacin Date: Fri, 21 Jun 2013 06:07:47 +0000 Subject: [PATCH] Better validation of the URL used in core HTTP requests. git-svn-id: http://core.svn.wordpress.org/trunk@24480 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/class-wp-importer.php | 1 + wp-admin/includes/file.php | 2 +- wp-includes/class-feed.php | 12 +++-- wp-includes/class-http.php | 11 ++++- wp-includes/class-oembed.php | 4 +- wp-includes/class-wp-xmlrpc-server.php | 3 +- wp-includes/comment.php | 58 ++--------------------- wp-includes/functions.php | 4 +- wp-includes/http.php | 61 +++++++++++++++++++++++++ wp-includes/rss.php | 2 +- 10 files changed, 92 insertions(+), 66 deletions(-) diff --git a/wp-admin/includes/class-wp-importer.php b/wp-admin/includes/class-wp-importer.php index 0cfc9fe8ab..0268e7e6ee 100644 --- a/wp-admin/includes/class-wp-importer.php +++ b/wp-admin/includes/class-wp-importer.php @@ -183,6 +183,7 @@ class WP_Importer { $headers = array(); $args = array(); + $args['reject_unsafe_urls'] = true; if ( true === $head ) $args['method'] = 'HEAD'; if ( !empty( $username ) && !empty( $password ) ) diff --git a/wp-admin/includes/file.php b/wp-admin/includes/file.php index c220586c35..bb192fae84 100644 --- a/wp-admin/includes/file.php +++ b/wp-admin/includes/file.php @@ -497,7 +497,7 @@ function download_url( $url, $timeout = 300 ) { if ( ! $tmpfname ) return new WP_Error('http_no_file', __('Could not create Temporary file.')); - $response = wp_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname ) ); + $response = wp_remote_get( $url, array( 'timeout' => $timeout, 'stream' => true, 'filename' => $tmpfname, 'reject_unsafe_urls' => true ) ); if ( is_wp_error( $response ) ) { unlink( $tmpfname ); diff --git a/wp-includes/class-feed.php b/wp-includes/class-feed.php index c442050c12..491d775e14 100644 --- a/wp-includes/class-feed.php +++ b/wp-includes/class-feed.php @@ -66,7 +66,11 @@ class WP_SimplePie_File extends SimplePie_File { $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE; if ( preg_match('/^http(s)?:\/\//i', $url) ) { - $args = array( 'timeout' => $this->timeout, 'redirection' => $this->redirects); + $args = array( + 'timeout' => $this->timeout, + 'redirection' => $this->redirects, + 'reject_unsafe_urls' => true, + ); if ( !empty($this->headers) ) $args['headers'] = $this->headers; @@ -85,10 +89,8 @@ class WP_SimplePie_File extends SimplePie_File { $this->status_code = wp_remote_retrieve_response_code( $res ); } } else { - if ( ! file_exists($url) || ( ! $this->body = file_get_contents($url) ) ) { - $this->error = 'file_get_contents could not read the file'; - $this->success = false; - } + $this->error = ''; + $this->success = false; } } } diff --git a/wp-includes/class-http.php b/wp-includes/class-http.php index ffb2aebb3c..6f6a08a22b 100644 --- a/wp-includes/class-http.php +++ b/wp-includes/class-http.php @@ -86,7 +86,8 @@ class WP_Http { 'timeout' => apply_filters( 'http_request_timeout', 5), 'redirection' => apply_filters( 'http_request_redirection_count', 5), 'httpversion' => apply_filters( 'http_request_version', '1.0'), - 'user-agent' => apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ), + 'user-agent' => apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ), + 'reject_unsafe_urls' => apply_filters( 'http_request_reject_unsafe_urls', false ), 'blocking' => true, 'headers' => array(), 'cookies' => array(), @@ -118,7 +119,11 @@ class WP_Http { if ( false !== $pre ) return $pre; - $arrURL = parse_url( $url ); + if ( $r['reject_unsafe_urls'] ) + $url = wp_http_validate_url( $url ); + $url = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) ); + + $arrURL = @parse_url( $url ); if ( empty( $url ) || empty( $arrURL['scheme'] ) ) return new WP_Error('http_request_failed', __('A valid URL was not provided.')); @@ -1146,6 +1151,8 @@ class WP_Http_Curl { // The option doesn't work with safe mode or when open_basedir is set, and there's a // bug #17490 with redirected POST requests, so handle redirections outside Curl. curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false ); + if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4 + curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); switch ( $r['method'] ) { case 'HEAD': diff --git a/wp-includes/class-oembed.php b/wp-includes/class-oembed.php index a56cdc6008..b9c01d64a2 100644 --- a/wp-includes/class-oembed.php +++ b/wp-includes/class-oembed.php @@ -113,7 +113,7 @@ class WP_oEmbed { $providers = array(); // Fetch URL content - if ( $html = wp_remote_retrieve_body( wp_remote_get( $url ) ) ) { + if ( $html = wp_remote_retrieve_body( wp_remote_get( $url, array( 'reject_unsafe_urls' => true ) ) ) ) { // types that contain oEmbed provider URLs $linktypes = apply_filters( 'oembed_linktypes', array( @@ -195,7 +195,7 @@ class WP_oEmbed { */ function _fetch_with_format( $provider_url_with_args, $format ) { $provider_url_with_args = add_query_arg( 'format', $format, $provider_url_with_args ); - $response = wp_remote_get( $provider_url_with_args ); + $response = wp_remote_get( $provider_url_with_args, array( 'reject_unsafe_urls' => true ) ); if ( 501 == wp_remote_retrieve_response_code( $response ) ) return new WP_Error( 'not-implemented' ); if ( ! $body = wp_remote_retrieve_body( $response ) ) diff --git a/wp-includes/class-wp-xmlrpc-server.php b/wp-includes/class-wp-xmlrpc-server.php index 397a8f49b9..98bfe00d5b 100644 --- a/wp-includes/class-wp-xmlrpc-server.php +++ b/wp-includes/class-wp-xmlrpc-server.php @@ -5396,7 +5396,8 @@ class wp_xmlrpc_server extends IXR_Server { sleep(1); // Let's check the remote site - $linea = wp_remote_retrieve_body( wp_remote_get( $pagelinkedfrom, array( 'timeout' => 10, 'redirection' => 0 ) ) ); + $linea = wp_remote_retrieve_body( wp_remote_get( $pagelinkedfrom, array( 'timeout' => 10, 'redirection' => 0, 'reject_unsafe_urls' => true ) ) ); + if ( !$linea ) return $this->pingback_error( 16, __( 'The source URL does not exist.' ) ); diff --git a/wp-includes/comment.php b/wp-includes/comment.php index 983d90561c..2795d9f720 100644 --- a/wp-includes/comment.php +++ b/wp-includes/comment.php @@ -1658,7 +1658,7 @@ function discover_pingback_server_uri( $url, $deprecated = '' ) { if ( 0 === strpos($url, $uploads_dir['baseurl']) ) return false; - $response = wp_remote_head( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) ); + $response = wp_remote_head( $url, array( 'timeout' => 2, 'httpversion' => '1.0', 'reject_unsafe_urls' => true ) ); if ( is_wp_error( $response ) ) return false; @@ -1671,7 +1671,7 @@ function discover_pingback_server_uri( $url, $deprecated = '' ) { return false; // Now do a GET since we're going to look in the html headers (and we're sure it's not a binary file) - $response = wp_remote_get( $url, array( 'timeout' => 2, 'httpversion' => '1.0' ) ); + $response = wp_remote_get( $url, array( 'timeout' => 2, 'httpversion' => '1.0', 'reject_unsafe_urls' => true ) ); if ( is_wp_error( $response ) ) return false; @@ -1906,6 +1906,7 @@ function trackback($trackback_url, $title, $excerpt, $ID) { $options = array(); $options['timeout'] = 4; + $options['reject_unsafe_urls'] = true; $options['body'] = array( 'title' => $title, 'url' => get_permalink($ID), @@ -1953,62 +1954,13 @@ function weblog_ping($server = '', $path = '') { * Default filter attached to pingback_ping_source_uri to validate the pingback's Source URI * * @since 3.5.1 + * @see wp_http_validate_url() * * @param string $source_uri * @return string */ function pingback_ping_source_uri( $source_uri ) { - $uri = esc_url_raw( $source_uri, array( 'http', 'https' ) ); - if ( ! $uri ) - return ''; - - $parsed_url = @parse_url( $uri ); - if ( ! $parsed_url ) - return ''; - - if ( isset( $parsed_url['user'] ) || isset( $parsed_url['pass'] ) ) - return ''; - - if ( false !== strpos( $parsed_url['host'], ':' ) ) - return ''; - - $parsed_home = @parse_url( get_option( 'home' ) ); - - $same_host = strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] ); - - if ( ! $same_host ) { - $host = trim( $parsed_url['host'], '.' ); - if ( preg_match( '#^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $host ) ) { - $ip = $host; - } else { - $ip = gethostbyname( $host ); - if ( $ip === $host ) // Error condition for gethostbyname() - $ip = false; - } - if ( $ip ) { - if ( '127.0.0.1' === $ip ) - return ''; - $parts = array_map( 'intval', explode( '.', $ip ) ); - if ( 10 === $parts[0] ) - return ''; - if ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] ) - return ''; - if ( 192 === $parts[0] && 168 === $parts[1] ) - return ''; - } - } - - if ( empty( $parsed_url['port'] ) ) - return $uri; - - $port = $parsed_url['port']; - if ( 80 === $port || 443 === $port || 8080 === $port ) - return $uri; - - if ( $parsed_home && $same_host && $parsed_home['port'] === $port ) - return $uri; - - return ''; + return (string) wp_http_validate_url( $source_uri ); } /** diff --git a/wp-includes/functions.php b/wp-includes/functions.php index a8efacfe85..b6739ecf78 100644 --- a/wp-includes/functions.php +++ b/wp-includes/functions.php @@ -496,6 +496,7 @@ function wp_get_http( $url, $file_path = false, $red = 1 ) { $options = array(); $options['redirection'] = 5; + $options['reject_unsafe_urls'] = true; if ( false == $file_path ) $options['method'] = 'HEAD'; @@ -543,7 +544,7 @@ function wp_get_http_headers( $url, $deprecated = false ) { if ( !empty( $deprecated ) ) _deprecated_argument( __FUNCTION__, '2.7' ); - $response = wp_remote_head( $url ); + $response = wp_remote_head( $url, array( 'reject_unsafe_urls' => true ) ); if ( is_wp_error( $response ) ) return false; @@ -758,6 +759,7 @@ function wp_remote_fopen( $uri ) { $options = array(); $options['timeout'] = 10; + $options['reject_unsafe_urls'] = true; $response = wp_remote_get( $uri, $options ); diff --git a/wp-includes/http.php b/wp-includes/http.php index acd273dbb8..2eb613c46b 100644 --- a/wp-includes/http.php +++ b/wp-includes/http.php @@ -330,3 +330,64 @@ function send_origin_headers() { return false; } + +/** + * Validate a URL for safe use in the HTTP API. + * + * @since 3.5.2 + * + * @return mixed URL or false on failure. + */ +function wp_http_validate_url( $url ) { + $url = esc_url_raw( $url, array( 'http', 'https' ) ); + if ( ! $url ) + return false; + + $parsed_url = @parse_url( $url ); + if ( ! $parsed_url ) + return false; + + if ( isset( $parsed_url['user'] ) || isset( $parsed_url['pass'] ) ) + return false; + + if ( false !== strpos( $parsed_url['host'], ':' ) ) + return false; + + $parsed_home = @parse_url( get_option( 'home' ) ); + + $same_host = strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] ); + + if ( ! $same_host ) { + $host = trim( $parsed_url['host'], '.' ); + if ( preg_match( '#^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $host ) ) { + $ip = $host; + } else { + $ip = gethostbyname( $host ); + if ( $ip === $host ) // Error condition for gethostbyname() + $ip = false; + } + if ( $ip ) { + if ( '127.0.0.1' === $ip ) + return false; + $parts = array_map( 'intval', explode( '.', $ip ) ); + if ( 10 === $parts[0] ) + return false; + if ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] ) + return false; + if ( 192 === $parts[0] && 168 === $parts[1] ) + return false; + } + } + + if ( empty( $parsed_url['port'] ) ) + return $url; + + $port = $parsed_url['port']; + if ( 80 === $port || 443 === $port || 8080 === $port ) + return $url; + + if ( $parsed_home && $same_host && $parsed_home['port'] === $port ) + return $url; + + return false; +} diff --git a/wp-includes/rss.php b/wp-includes/rss.php index 4797d4c4a7..d064020c0c 100644 --- a/wp-includes/rss.php +++ b/wp-includes/rss.php @@ -536,7 +536,7 @@ endif; * @return Snoopy style response */ function _fetch_remote_file($url, $headers = "" ) { - $resp = wp_remote_request($url, array('headers' => $headers, 'timeout' => MAGPIE_FETCH_TIME_OUT)); + $resp = wp_remote_request($url, array('headers' => $headers, 'timeout' => MAGPIE_FETCH_TIME_OUT, 'reject_unsafe_urls' => true )); if ( is_wp_error($resp) ) { $error = array_shift($resp->errors);