From 2183725c5437c936f6f436527a1137028e37bc86 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 29 Sep 2015 00:40:30 +0000 Subject: [PATCH] Avoid stripping square brackets from URLs, and instead correctly encode them. Square brackets must be encoded in the path, path parameters, query parameters, and fragment, but must not be encoded in anything up to the domain and port. Adds a bunch of tests, including square brackets in query parameters, IPv6 URLs, and several other permutations. See #16859 git-svn-id: https://develop.svn.wordpress.org/trunk@34674 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/formatting.php | 51 +++++++++++++-- tests/phpunit/tests/formatting/EscUrl.php | 76 +++++++++++++++++++++-- 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 7140dc2c4e..779003e7df 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -2077,15 +2077,17 @@ function _make_web_ftp_clickable_cb( $matches ) { $ret = ''; $dest = $matches[2]; $dest = 'http://' . $dest; - $dest = esc_url($dest); - if ( empty($dest) ) - return $matches[0]; // removed trailing [.,;:)] from URL if ( in_array( substr($dest, -1), array('.', ',', ';', ':', ')') ) === true ) { $ret = substr($dest, -1); $dest = substr($dest, 0, strlen($dest)-1); } + + $dest = esc_url($dest); + if ( empty($dest) ) + return $matches[0]; + return $matches[1] . "$dest$ret"; } @@ -3278,6 +3280,8 @@ function esc_sql( $data ) { * (the default behaviour) ampersands are also replaced. The 'clean_url' filter * is applied to the returned cleaned URL. * + * See RFC3986 + * * @since 2.8.0 * * @param string $url The URL to be cleaned. @@ -3293,7 +3297,7 @@ function esc_url( $url, $protocols = null, $_context = 'display' ) { return $url; $url = str_replace( ' ', '%20', $url ); - $url = preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\\x80-\\xff]|i', '', $url); + $url = preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\\x80-\\xff]|i', '', $url); if ( '' === $url ) { return $url; @@ -3306,7 +3310,7 @@ function esc_url( $url, $protocols = null, $_context = 'display' ) { $url = str_replace(';//', '://', $url); /* If the URL doesn't appear to contain a scheme, we - * presume it needs http:// appended (unless a relative + * presume it needs http:// prepended (unless a relative * link starting with /, # or ? or a php file). */ if ( strpos($url, ':') === false && ! in_array( $url[0], array( '/', '#', '?' ) ) && @@ -3320,6 +3324,43 @@ function esc_url( $url, $protocols = null, $_context = 'display' ) { $url = str_replace( "'", ''', $url ); } + if ( ( false !== strpos( $url, '[' ) ) || ( false !== strpos( $url, ']' ) ) ) { + + $parsed = parse_url( $url ); + $front = ''; + + if ( isset( $parsed['scheme'] ) ) { + $front .= $parsed['scheme'] . '://'; + } elseif ( '/' === $url[0] ) { + $front .= '//'; + } + + if ( isset( $parsed['user'] ) ) { + $front .= $parsed['user']; + } + + if ( isset( $parsed['pass'] ) ) { + $front .= ':' . $parsed['pass']; + } + + if ( isset( $parsed['user'] ) || isset( $parsed['pass'] ) ) { + $front .= '@'; + } + + if ( isset( $parsed['host'] ) ) { + $front .= $parsed['host']; + } + + if ( isset( $parsed['port'] ) ) { + $front .= ':' . $parsed['port']; + } + + $end_dirty = str_replace( $front, '', $url ); + $end_clean = str_replace( array( '[', ']' ), array( '%5B', '%5D' ), $end_dirty ); + $url = str_replace( $end_dirty, $end_clean, $url ); + + } + if ( '/' === $url[0] ) { $good_protocol_url = $url; } else { diff --git a/tests/phpunit/tests/formatting/EscUrl.php b/tests/phpunit/tests/formatting/EscUrl.php index 1289fd7c76..35cc39865f 100644 --- a/tests/phpunit/tests/formatting/EscUrl.php +++ b/tests/phpunit/tests/formatting/EscUrl.php @@ -40,15 +40,41 @@ class Tests_Formatting_EscUrl extends WP_UnitTestCase { } function test_all_url_parts() { - $url = 'https://user:password@host.example.com:1234/path;p=1?q=2&r=3#fragment'; - $this->assertEquals( $url, esc_url_raw( $url ) ); + $url = 'https://user:pass@host.example.com:1234/path;p=1?query=2&r[]=3#fragment'; - $this->assertEquals( 'https://user:password@host.example.com:1234/path;p=1?q=2&r=3#fragment', esc_url( $url ) ); + $this->assertEquals( array( + 'scheme' => 'https', + 'host' => 'host.example.com', + 'port' => 1234, + 'user' => 'user', + 'pass' => 'pass', + 'path' => '/path;p=1', + 'query' => 'query=2&r[]=3', + 'fragment' => 'fragment', + ), parse_url( $url ) ); + $this->assertEquals( 'https://user:pass@host.example.com:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url_raw( $url ) ); + $this->assertEquals( 'https://user:pass@host.example.com:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url( $url ) ); + } - $this->assertEquals( 'http://example.com?foo', esc_url( 'http://example.com?foo' ) ); + function test_all_url_parts_ipv6() { + $url = 'https://user:pass@[::FFFF::127.0.0.1]:1234/path;p=1?query=2&r[]=3#fragment'; + + $this->assertEquals( array( + 'scheme' => 'https', + 'host' => '[::FFFF::127.0.0.1]', + 'port' => 1234, + 'user' => 'user', + 'pass' => 'pass', + 'path' => '/path;p=1', + 'query' => 'query=2&r[]=3', + 'fragment' => 'fragment', + ), parse_url( $url ) ); + $this->assertEquals( 'https://user:pass@[::FFFF::127.0.0.1]:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url_raw( $url ) ); + $this->assertEquals( 'https://user:pass@[::FFFF::127.0.0.1]:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url( $url ) ); } function test_bare() { + $this->assertEquals( 'http://example.com?foo', esc_url( 'example.com?foo' ) ); $this->assertEquals( 'http://example.com', esc_url( 'example.com' ) ); $this->assertEquals( 'http://localhost', esc_url( 'localhost' ) ); $this->assertEquals( 'http://example.com/foo', esc_url( 'example.com/foo' ) ); @@ -125,6 +151,46 @@ class Tests_Formatting_EscUrl extends WP_UnitTestCase { $this->assertEquals( 'feed:http://wordpress.org/feed/', esc_url( 'feed:http://wordpress.org/feed/' ) ); } + /** + * @ticket 16859 + */ + function test_square_brackets() { + $this->assertEquals( '/example.php?one%5B%5D=two', esc_url( '/example.php?one[]=two' ) ); + $this->assertEquals( '?foo%5Bbar%5D=baz', esc_url( '?foo[bar]=baz' ) ); + $this->assertEquals( '//example.com/?foo%5Bbar%5D=baz', esc_url( '//example.com/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://example.com/?foo%5Bbar%5D=baz', esc_url( 'example.com/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://localhost?foo%5Bbar%5D=baz', esc_url( 'localhost?foo[bar]=baz' ) ); + $this->assertEquals( 'http://user:pass@localhost/?foo%5Bbar%5D=baz', esc_url( 'http://user:pass@localhost/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://localhost?foo%5Bbar%5D=baz', esc_url( 'localhost?foo[bar]=baz' ) ); + $this->assertEquals( 'http://example.com/?foo%5Bbar%5D=baz', esc_url( 'http://example.com/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://example.com:1234/?foo%5Bbar%5D=baz', esc_url( 'http://example.com:1234/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://example.com/?foo%5Bbar%5D=baz', esc_url( 'http://example.com/?foo%5Bbar%5D=baz' ) ); + $this->assertEquals( 'http://example.com/?baz=bar&foo%5Bbar%5D=baz', esc_url( 'http://example.com/?baz=bar&foo[bar]=baz' ) ); + $this->assertEquals( 'http://example.com/?baz=bar&foo%5Bbar%5D=baz', esc_url( 'http://example.com/?baz=bar&foo%5Bbar%5D=baz' ) ); + } + + /** + * @ticket 16859 + */ + function test_ipv6_hosts() { + $this->assertEquals( '//[::127.0.0.1]', esc_url( '//[::127.0.0.1]' ) ); + $this->assertEquals( 'http://[::FFFF::127.0.0.1]', esc_url( 'http://[::FFFF::127.0.0.1]' ) ); + $this->assertEquals( 'http://[::127.0.0.1]', esc_url( 'http://[::127.0.0.1]' ) ); + $this->assertEquals( 'http://[::DEAD:BEEF:DEAD:BEEF:DEAD:BEEF:DEAD:BEEF]', esc_url( 'http://[::DEAD:BEEF:DEAD:BEEF:DEAD:BEEF:DEAD:BEEF]' ) ); + + // IPv6 with square brackets in the query? Why not. + $this->assertEquals( '//[::FFFF::127.0.0.1]/?foo%5Bbar%5D=baz', esc_url( '//[::FFFF::127.0.0.1]/?foo[bar]=baz' ) ); + $this->assertEquals( 'http://[::FFFF::127.0.0.1]/?foo%5Bbar%5D=baz', esc_url( 'http://[::FFFF::127.0.0.1]/?foo[bar]=baz' ) ); + } + + /** + * Courtesy of http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding + */ + function test_reserved_characters() { + $url = "http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$%27()*+,;=/?:@-._~!$%27()*+,;==#/?:@-._~!$&'()*+,;="; + $this->assertEquals( $url, esc_url_raw( $url ) ); + } + /** * @ticket 21974 */ @@ -175,7 +241,7 @@ EOT; * @ticket 28015 */ function test_invalid_charaters() { - $this->assertEmpty( esc_url_raw('"^[]<>{}`') ); + $this->assertEmpty( esc_url_raw('"^<>{}`') ); } }