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
This commit is contained in:
John Blackbourn 2015-09-29 00:40:30 +00:00
parent 8f4a6370f0
commit 2183725c54
2 changed files with 117 additions and 10 deletions

View File

@ -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] . "<a href=\"$dest\" rel=\"nofollow\">$dest</a>$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( "'", '&#039;', $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 {

View File

@ -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&#038;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&#038;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&#038;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&#038;foo%5Bbar%5D=baz', esc_url( 'http://example.com/?baz=bar&foo[bar]=baz' ) );
$this->assertEquals( 'http://example.com/?baz=bar&#038;foo%5Bbar%5D=baz', esc_url( 'http://example.com/?baz=bar&#038;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('"^<>{}`') );
}
}