From 9d1455b89b4091a2464c940e5ad2d342b433700c Mon Sep 17 00:00:00 2001 From: "Dominik Schilling (ocean90)" Date: Tue, 2 Feb 2016 16:58:49 +0000 Subject: [PATCH] Better validation of the URL used in HTTP redirects. Merges [36444] to the 4.4 branch. git-svn-id: https://develop.svn.wordpress.org/branches/4.4@36447 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/pluggable.php | 15 ++- tests/phpunit/tests/formatting/redirect.php | 105 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 88d1440599..3159b37e12 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -1333,7 +1333,8 @@ function wp_validate_redirect($location, $default = '') { // In php 5 parse_url may fail if the URL query part contains http://, bug #38143 $test = ( $cut = strpos($location, '?') ) ? substr( $location, 0, $cut ) : $location; - $lp = parse_url($test); + // @-operator is used to prevent possible warnings in PHP < 5.3.3. + $lp = @parse_url($test); // Give up if malformed URL if ( false === $lp ) @@ -1343,9 +1344,17 @@ function wp_validate_redirect($location, $default = '') { if ( isset($lp['scheme']) && !('http' == $lp['scheme'] || 'https' == $lp['scheme']) ) return $default; - // Reject if scheme is set but host is not. This catches urls like https:host.com for which parse_url does not set the host field. - if ( isset($lp['scheme']) && !isset($lp['host']) ) + // Reject if certain components are set but host is not. This catches urls like https:host.com for which parse_url does not set the host field. + if ( ! isset( $lp['host'] ) && ( isset( $lp['scheme'] ) || isset( $lp['user'] ) || isset( $lp['pass'] ) || isset( $lp['port'] ) ) ) { return $default; + } + + // Reject malformed components parse_url() can return on odd inputs + foreach ( array( 'user', 'pass', 'host' ) as $component ) { + if ( isset( $lp[ $component ] ) && strpbrk( $lp[ $component ], ':/?#@' ) ) { + return $default; + } + } $wpp = parse_url(home_url()); diff --git a/tests/phpunit/tests/formatting/redirect.php b/tests/phpunit/tests/formatting/redirect.php index 1a89a613de..8628bbf282 100644 --- a/tests/phpunit/tests/formatting/redirect.php +++ b/tests/phpunit/tests/formatting/redirect.php @@ -3,8 +3,21 @@ /** * @group pluggable * @group formatting + * @group redirect */ class Tests_Formatting_Redirect extends WP_UnitTestCase { + function setUp() { + add_filter( 'home_url', array( $this, 'home_url' ) ); + } + + function tearDown() { + remove_filter( 'home_url', array( $this, 'home_url' ) ); + } + + function home_url() { + return 'http://example.com/'; + } + function test_wp_sanitize_redirect() { $this->assertEquals('http://example.com/watchthelinefeedgo', wp_sanitize_redirect('http://example.com/watchthelinefeed%0Ago')); $this->assertEquals('http://example.com/watchthelinefeedgo', wp_sanitize_redirect('http://example.com/watchthelinefeed%0ago')); @@ -20,4 +33,96 @@ class Tests_Formatting_Redirect extends WP_UnitTestCase { $this->assertEquals('http://example.com/search.php?search=(amistillhere)', wp_sanitize_redirect('http://example.com/search.php?search=(amistillhere)')); $this->assertEquals('http://example.com/@username', wp_sanitize_redirect('http://example.com/@username')); } + + /** + * @dataProvider valid_url_provider + */ + function test_wp_validate_redirect_valid_url( $url, $expected ) { + $this->assertEquals( $expected, wp_validate_redirect( $url ) ); + } + + /** + * @dataProvider invalid_url_provider + */ + function test_wp_validate_redirect_invalid_url( $url ) { + $this->assertEquals( false, wp_validate_redirect( $url, false ) ); + } + + function valid_url_provider() { + return array( + array( 'http://example.com', 'http://example.com' ), + array( 'http://example.com/', 'http://example.com/' ), + array( 'https://example.com/', 'https://example.com/' ), + array( '//example.com', 'http://example.com' ), + array( '//example.com/', 'http://example.com/' ), + array( 'http://example.com/?foo=http://example.com/', 'http://example.com/?foo=http://example.com/' ), + array( 'http://user@example.com/', 'http://user@example.com/' ), + array( 'http://user:@example.com/', 'http://user:@example.com/' ), + array( 'http://user:pass@example.com/', 'http://user:pass@example.com/' ), + ); + } + + function invalid_url_provider() { + return array( + // parse_url() fails + array( '' ), + array( 'http://:' ), + + // non-safelisted domain + array( 'http://non-safelisted.example/' ), + + // unsupported schemes + array( 'data:text/plain;charset=utf-8,Hello%20World!' ), + array( 'file:///etc/passwd' ), + array( 'ftp://example.com/' ), + + // malformed input + array( 'http:example.com' ), + array( 'http:80' ), + array( 'http://example.com:1234:5678/' ), + array( 'http://user:pa:ss@example.com/' ), + + array( 'http://user@@example.com' ), + array( 'http://user@:example.com' ), + array( 'http://user?@example.com' ), + array( 'http://user@?example.com' ), + array( 'http://user#@example.com' ), + array( 'http://user@#example.com' ), + + array( 'http://user@@example.com/' ), + array( 'http://user@:example.com/' ), + array( 'http://user?@example.com/' ), + array( 'http://user@?example.com/' ), + array( 'http://user#@example.com/' ), + array( 'http://user@#example.com/' ), + + array( 'http://user:pass@@example.com' ), + array( 'http://user:pass@:example.com' ), + array( 'http://user:pass?@example.com' ), + array( 'http://user:pass@?example.com' ), + array( 'http://user:pass#@example.com' ), + array( 'http://user:pass@#example.com' ), + + array( 'http://user:pass@@example.com/' ), + array( 'http://user:pass@:example.com/' ), + array( 'http://user:pass?@example.com/' ), + array( 'http://user:pass@?example.com/' ), + array( 'http://user:pass#@example.com/' ), + array( 'http://user:pass@#example.com/' ), + + array( 'http://user.pass@@example.com' ), + array( 'http://user.pass@:example.com' ), + array( 'http://user.pass?@example.com' ), + array( 'http://user.pass@?example.com' ), + array( 'http://user.pass#@example.com' ), + array( 'http://user.pass@#example.com' ), + + array( 'http://user.pass@@example.com/' ), + array( 'http://user.pass@:example.com/' ), + array( 'http://user.pass?@example.com/' ), + array( 'http://user.pass@?example.com/' ), + array( 'http://user.pass#@example.com/' ), + array( 'http://user.pass@#example.com/' ), + ); + } }