Skip to content

Commit 30eb659

Browse files
committed
Application Passwords: Allow HTTP loopback redirect URLs
This change allows HTTP redirect URLs for loopback addresses (`127.0.0.1`, `[::1]`) in `wp_is_authorize_application_redirect_url_valid()`, regardless of environment type. This aligns the application password implementation with RFC 8252 7.3. It's worth noting that section 8.3 of the RFC recommends against allowing `localhost` as a loopback redirect, since it may be susceptible to firewall interception and DNS resolution poisoning. Props aquarius, pento. Fixes #57809. git-svn-id: https://develop.svn.wordpress.org/trunk@62096 602fd350-edb4-49c9-b593-d223f7449a82
1 parent ece2d36 commit 30eb659

File tree

3 files changed

+209
-2
lines changed

3 files changed

+209
-2
lines changed

src/wp-admin/includes/user.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,10 @@ function wp_is_authorize_application_password_request_valid( $request, $user ) {
700700
}
701701

702702
/**
703-
* Validates the redirect URL protocol scheme. The protocol can be anything except `http` and `javascript`.
703+
* Validates the redirect URL protocol scheme.
704+
*
705+
* The `http` scheme is allowed for loopback IP addresses (127.0.0.1, [::1])
706+
* and local environments. The `javascript` and `data` protocols are always rejected.
704707
*
705708
* @since 6.3.2
706709
*
@@ -745,7 +748,14 @@ function wp_is_authorize_application_redirect_url_valid( $url ) {
745748
);
746749
}
747750

748-
if ( 'http' === $scheme && ! $is_local ) {
751+
// Allow insecure HTTP connections to locally hosted applications.
752+
$is_loopback = in_array(
753+
strtolower( $host ),
754+
array( '127.0.0.1', '[::1]' ),
755+
true
756+
);
757+
758+
if ( 'http' === $scheme && ! $is_local && ! $is_loopback ) {
749759
return new WP_Error(
750760
'invalid_redirect_scheme',
751761
__( 'The URL must be served over a secure connection.' )

tests/phpunit/tests/admin/Admin_Includes_User_WpIsAuthorizeApplicationPasswordRequestValid_Test.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ public function data_is_authorize_application_password_request_valid() {
8181
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
8282
'env' => $environment_type,
8383
);
84+
85+
$datasets[ $environment_type . ' and a "http" loopback "success_url"' ] = array(
86+
'request' => array( 'success_url' => 'http://127.0.0.1:8080/callback' ),
87+
'expected_error_code' => '',
88+
'env' => $environment_type,
89+
);
90+
91+
$datasets[ $environment_type . ' and a "http" loopback "reject_url"' ] = array(
92+
'request' => array( 'reject_url' => 'http://127.0.0.1/callback' ),
93+
'expected_error_code' => '',
94+
'env' => $environment_type,
95+
);
8496
}
8597

8698
return $datasets;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
/**
4+
* @group admin
5+
* @group user
6+
*
7+
* @covers ::wp_is_authorize_application_redirect_url_valid
8+
*/
9+
class Admin_Includes_User_WpIsAuthorizeApplicationRedirectUrlValid_Test extends WP_UnitTestCase {
10+
11+
/**
12+
* Test redirect URL validation for application password authorization.
13+
*
14+
* @ticket 57809
15+
*
16+
* @dataProvider data_wp_is_authorize_application_redirect_url_valid
17+
*
18+
* @param string $url The redirect URL to validate.
19+
* @param string $expected_error_code The expected error code, empty if no error is expected.
20+
* @param string $env The environment type. Defaults to 'production'.
21+
*/
22+
public function test_wp_is_authorize_application_redirect_url_valid( $url, $expected_error_code, $env = 'production' ) {
23+
putenv( "WP_ENVIRONMENT_TYPE=$env" );
24+
25+
$actual = wp_is_authorize_application_redirect_url_valid( $url );
26+
27+
putenv( 'WP_ENVIRONMENT_TYPE' );
28+
29+
if ( $expected_error_code ) {
30+
$this->assertWPError( $actual, 'A WP_Error object is expected.' );
31+
$this->assertSame( $expected_error_code, $actual->get_error_code(), 'Unexpected error code.' );
32+
} else {
33+
$this->assertTrue( $actual, 'The URL should be considered valid.' );
34+
}
35+
}
36+
37+
/**
38+
* Data provider for test_wp_is_authorize_application_redirect_url_valid.
39+
*
40+
* @return array[]
41+
*/
42+
public function data_wp_is_authorize_application_redirect_url_valid() {
43+
$environment_types = array( 'local', 'development', 'staging', 'production' );
44+
45+
$datasets = array();
46+
foreach ( $environment_types as $environment_type ) {
47+
// Empty URL should always be valid.
48+
$datasets[ $environment_type . ' and an empty URL' ] = array(
49+
'url' => '',
50+
'expected_error_code' => '',
51+
'env' => $environment_type,
52+
);
53+
54+
// HTTPS URLs should always be valid.
55+
$datasets[ $environment_type . ' and a "https" scheme URL' ] = array(
56+
'url' => 'https://example.org',
57+
'expected_error_code' => '',
58+
'env' => $environment_type,
59+
);
60+
61+
$datasets[ $environment_type . ' and a "https" scheme URL with path' ] = array(
62+
'url' => 'https://example.org/callback',
63+
'expected_error_code' => '',
64+
'env' => $environment_type,
65+
);
66+
67+
// Custom app schemes should always be valid.
68+
$datasets[ $environment_type . ' and a custom app scheme URL' ] = array(
69+
'url' => 'wordpress://callback',
70+
'expected_error_code' => '',
71+
'env' => $environment_type,
72+
);
73+
74+
$datasets[ $environment_type . ' and another custom app scheme URL' ] = array(
75+
'url' => 'myapp://auth/callback',
76+
'expected_error_code' => '',
77+
'env' => $environment_type,
78+
);
79+
80+
// Invalid protocols should always be rejected.
81+
$datasets[ $environment_type . ' and a "javascript" scheme URL' ] = array(
82+
'url' => 'javascript://example.org/%0Aalert(1)',
83+
'expected_error_code' => 'invalid_redirect_url_format',
84+
'env' => $environment_type,
85+
);
86+
87+
$datasets[ $environment_type . ' and a "data" scheme URL' ] = array(
88+
'url' => 'data://text/html,test',
89+
'expected_error_code' => 'invalid_redirect_url_format',
90+
'env' => $environment_type,
91+
);
92+
93+
// Invalid URL formats should always be rejected.
94+
$datasets[ $environment_type . ' and a URL without a valid scheme' ] = array(
95+
'url' => 'not-a-url',
96+
'expected_error_code' => 'invalid_redirect_url_format',
97+
'env' => $environment_type,
98+
);
99+
100+
$datasets[ $environment_type . ' and a URL with an empty host' ] = array(
101+
'url' => 'http://',
102+
'expected_error_code' => 'invalid_redirect_url_format',
103+
'env' => $environment_type,
104+
);
105+
106+
// HTTP + loopback IP addresses should be valid in all environments.
107+
$datasets[ $environment_type . ' and a "http" scheme URL with 127.0.0.1' ] = array(
108+
'url' => 'http://127.0.0.1/callback',
109+
'expected_error_code' => '',
110+
'env' => $environment_type,
111+
);
112+
113+
$datasets[ $environment_type . ' and a "http" scheme URL with IPv6 loopback' ] = array(
114+
'url' => 'http://[::1]/callback',
115+
'expected_error_code' => '',
116+
'env' => $environment_type,
117+
);
118+
119+
// HTTP + loopback IP addresses with ports should be valid in all environments.
120+
$datasets[ $environment_type . ' and a "http" scheme URL with 127.0.0.1 and port' ] = array(
121+
'url' => 'http://127.0.0.1:3000/callback',
122+
'expected_error_code' => '',
123+
'env' => $environment_type,
124+
);
125+
126+
$datasets[ $environment_type . ' and a "http" scheme URL with IPv6 loopback and port' ] = array(
127+
'url' => 'http://[::1]:8080/callback',
128+
'expected_error_code' => '',
129+
'env' => $environment_type,
130+
);
131+
132+
// HTTP + non-loopback host should only be valid in local environments.
133+
$datasets[ $environment_type . ' and a "http" scheme URL with a non-loopback host' ] = array(
134+
'url' => 'http://example.org',
135+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
136+
'env' => $environment_type,
137+
);
138+
139+
$datasets[ $environment_type . ' and a "http" scheme URL with a non-loopback host and path' ] = array(
140+
'url' => 'http://example.org/callback',
141+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
142+
'env' => $environment_type,
143+
);
144+
145+
// Boundary cases: hostnames and addresses NOT treated as loopback.
146+
$datasets[ $environment_type . ' and a "http" scheme URL with localhost' ] = array(
147+
'url' => 'http://localhost/callback',
148+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
149+
'env' => $environment_type,
150+
);
151+
152+
$datasets[ $environment_type . ' and a "http" scheme URL with 127.0.0.2' ] = array(
153+
'url' => 'http://127.0.0.2/callback',
154+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
155+
'env' => $environment_type,
156+
);
157+
158+
$datasets[ $environment_type . ' and a "http" scheme URL with expanded IPv6 loopback' ] = array(
159+
'url' => 'http://[0:0:0:0:0:0:0:1]/callback',
160+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
161+
'env' => $environment_type,
162+
);
163+
164+
$datasets[ $environment_type . ' and a "http" scheme URL with localhost.localdomain' ] = array(
165+
'url' => 'http://localhost.localdomain/callback',
166+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
167+
'env' => $environment_type,
168+
);
169+
170+
$datasets[ $environment_type . ' and a "http" scheme URL with localhost as subdomain' ] = array(
171+
'url' => 'http://localhost.example.org/callback',
172+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
173+
'env' => $environment_type,
174+
);
175+
176+
$datasets[ $environment_type . ' and a "http" scheme URL with localhost as suffix' ] = array(
177+
'url' => 'http://examplelocalhost.org/callback',
178+
'expected_error_code' => 'local' === $environment_type ? '' : 'invalid_redirect_scheme',
179+
'env' => $environment_type,
180+
);
181+
}
182+
183+
return $datasets;
184+
}
185+
}

0 commit comments

Comments
 (0)