Skip to content

Commit bed3a7c

Browse files
HTTP API: Introduce 'http_allowed_safe_ports' filter in wp_http_validate_url().
Adds a new filter `'http_allowed_safe_ports'` to control which ports are allowed for remote requests. By default, ports 80, 443, and 8080 are allowed for safe remote requests. Adds tests. Follow-up to [24480]. Props xknown, johnbillion, jorbin, costdev, dd32. Fixes #54331. git-svn-id: https://develop.svn.wordpress.org/trunk@52084 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 16b7125 commit bed3a7c

File tree

2 files changed

+195
-8
lines changed

2 files changed

+195
-8
lines changed

src/wp-includes/http.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,10 @@ function send_origin_headers() {
514514
* @return string|false URL or false on failure.
515515
*/
516516
function wp_http_validate_url( $url ) {
517+
if ( ! is_string( $url ) || '' === $url || is_numeric( $url ) ) {
518+
return false;
519+
}
520+
517521
$original_url = $url;
518522
$url = wp_kses_bad_protocol( $url, array( 'http', 'https' ) );
519523
if ( ! $url || strtolower( $url ) !== strtolower( $original_url ) ) {
@@ -534,15 +538,10 @@ function wp_http_validate_url( $url ) {
534538
}
535539

536540
$parsed_home = parse_url( get_option( 'home' ) );
537-
538-
if ( isset( $parsed_home['host'] ) ) {
539-
$same_host = strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] );
540-
} else {
541-
$same_host = false;
542-
}
541+
$same_host = isset( $parsed_home['host'] ) && strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] );
542+
$host = trim( $parsed_url['host'], '.' );
543543

544544
if ( ! $same_host ) {
545-
$host = trim( $parsed_url['host'], '.' );
546545
if ( preg_match( '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', $host ) ) {
547546
$ip = $host;
548547
} else {
@@ -581,7 +580,20 @@ function wp_http_validate_url( $url ) {
581580
}
582581

583582
$port = $parsed_url['port'];
584-
if ( 80 === $port || 443 === $port || 8080 === $port ) {
583+
584+
/**
585+
* Controls the list of ports considered safe in HTTP API.
586+
*
587+
* Allows to change and allow external requests for the HTTP request.
588+
*
589+
* @since 5.9.0
590+
*
591+
* @param array $allowed_ports Array of integers for valid ports.
592+
* @param string $host Host name of the requested URL.
593+
* @param string $url Requested URL.
594+
*/
595+
$allowed_ports = apply_filters( 'http_allowed_safe_ports', array( 80, 443, 8080 ), $host, $url );
596+
if ( in_array( $port, $allowed_ports, true ) ) {
585597
return $url;
586598
}
587599

tests/phpunit/tests/http/http.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,179 @@ public function wp_translate_php_url_constant_to_key_testcases() {
392392
);
393393
}
394394

395+
/**
396+
* Test that wp_http_validate_url validates URLs.
397+
*
398+
* @ticket 54331
399+
*
400+
* @dataProvider data_wp_http_validate_url_should_validate
401+
*
402+
* @covers ::wp_http_validate_url
403+
*
404+
* @param string $url The URL to validate.
405+
* @param false|string $cb_safe_ports The name of the callback to http_allowed_safe_ports or false if none.
406+
* Default false.
407+
* @param bool $external_host Whether or not the host is external.
408+
* Default false.
409+
*/
410+
public function test_wp_http_validate_url_should_validate( $url, $cb_safe_ports = false, $external_host = false ) {
411+
if ( $external_host ) {
412+
add_filter( 'http_request_host_is_external', '__return_true' );
413+
}
414+
415+
if ( $cb_safe_ports ) {
416+
add_filter( 'http_allowed_safe_ports', array( $this, $cb_safe_ports ) );
417+
}
418+
419+
$this->assertSame( $url, wp_http_validate_url( $url ) );
420+
}
421+
422+
/**
423+
* Data provider.
424+
*
425+
* @return array
426+
*/
427+
public function data_wp_http_validate_url_should_validate() {
428+
return array(
429+
'no port specified' => array(
430+
'url' => 'http://example.com/caniload.php',
431+
),
432+
'an external request when allowed' => array(
433+
'url' => 'http://172.20.0.123/caniload.php',
434+
'cb_safe_ports' => false,
435+
'external_host' => true,
436+
),
437+
'a port considered safe by default' => array(
438+
'url' => 'https://example.com:8080/caniload.php',
439+
),
440+
'a port considered safe by filter' => array(
441+
'url' => 'https://example.com:81/caniload.php',
442+
'cb_safe_ports' => 'callback_custom_safe_ports',
443+
),
444+
);
445+
}
446+
447+
/**
448+
* Tests that wp_http_validate_url validates a url that uses an unsafe port
449+
* but which matches the host and port used by the site's home url.
450+
*
451+
* @ticket 54331
452+
*
453+
* @covers ::wp_http_validate_url
454+
*/
455+
public function test_wp_http_validate_url_should_validate_with_an_unsafe_port_when_the_host_and_port_match_the_home_url() {
456+
$original_home = get_option( 'home' );
457+
$home_parsed = parse_url( $original_home );
458+
$home_scheme_host = implode( '://', array_slice( $home_parsed, 0, 2 ) );
459+
$home_modified = $home_scheme_host . ':83';
460+
461+
update_option( 'home', $home_modified );
462+
463+
$url = $home_modified . '/caniload.php';
464+
$this->assertSame( $url, wp_http_validate_url( $url ) );
465+
466+
update_option( 'home', $original_home );
467+
}
468+
469+
/**
470+
* Test that wp_http_validate_url does not validate invalid URLs.
471+
*
472+
* @ticket 54331
473+
*
474+
* @dataProvider data_wp_http_validate_url_should_not_validate
475+
*
476+
* @covers ::wp_http_validate_url
477+
*
478+
* @param string $url The URL to validate.
479+
* @param false|string $cb_safe_ports The name of the callback to http_allowed_safe_ports or false if none.
480+
* Default false.
481+
* @param bool $external_host Whether or not the host is external.
482+
* Default false.
483+
*/
484+
public function test_wp_http_validate_url_should_not_validate( $url, $cb_safe_ports = false, $external_host = false ) {
485+
if ( $external_host ) {
486+
add_filter( 'http_request_host_is_external', '__return_true' );
487+
}
488+
489+
if ( $cb_safe_ports ) {
490+
add_filter( 'http_allowed_safe_ports', array( $this, $cb_safe_ports ) );
491+
}
492+
493+
$this->assertFalse( wp_http_validate_url( $url ) );
494+
}
495+
496+
/**
497+
* Data provider.
498+
*
499+
* @return array
500+
*/
501+
public function data_wp_http_validate_url_should_not_validate() {
502+
return array(
503+
'url as false' => array(
504+
'url' => false,
505+
),
506+
'url as null' => array(
507+
'url' => null,
508+
),
509+
'url as int 0' => array(
510+
'url' => 0,
511+
),
512+
'url as string 0' => array(
513+
'url' => '0',
514+
),
515+
'url as int 1' => array(
516+
'url' => 1,
517+
),
518+
'url as string 1' => array(
519+
'url' => '1',
520+
),
521+
'url as array()' => array(
522+
'url' => array(),
523+
),
524+
'an empty url' => array(
525+
'url' => '',
526+
),
527+
'a url with a non-http/https protocol' => array(
528+
'url' => 'ftp://example.com:81/caniload.php',
529+
),
530+
'a malformed url' => array(
531+
'url' => 'http:///example.com:81/caniload.php',
532+
),
533+
'a host that cannot be parsed' => array(
534+
'url' => 'http:example.com/caniload.php',
535+
),
536+
'login information' => array(
537+
'url' => 'http://user:pass@example.com/caniload.php',
538+
),
539+
'a host with invalid characters' => array(
540+
'url' => 'http://[exam]ple.com/caniload.php',
541+
),
542+
'a host whose IPv4 address cannot be resolved' => array(
543+
'url' => 'http://exampleeeee.com/caniload.php',
544+
),
545+
'an external request when not allowed' => array(
546+
'url' => 'http://192.168.0.1/caniload.php',
547+
'external_host' => false,
548+
),
549+
'a port not considered safe by default' => array(
550+
'url' => 'https://example.com:81/caniload.php',
551+
),
552+
'a port not considered safe by filter' => array(
553+
'url' => 'https://example.com:82/caniload.php',
554+
'cb_safe_ports' => 'callback_custom_safe_ports',
555+
),
556+
'all safe ports removed by filter' => array(
557+
'url' => 'https://example.com:81/caniload.php',
558+
'cb_safe_ports' => 'callback_remove_safe_ports',
559+
),
560+
);
561+
}
562+
563+
public function callback_custom_safe_ports( $ports ) {
564+
return array( 81, 444, 8081 );
565+
}
566+
567+
public function callback_remove_safe_ports( $ports ) {
568+
return array();
569+
}
395570
}

0 commit comments

Comments
 (0)