Skip to content

Commit 82774b0

Browse files
[5.x] Harden auth redirects (#14089)
Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent 25d1702 commit 82774b0

28 files changed

Lines changed: 629 additions & 113 deletions

src/Auth/ResetsPasswords.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Illuminate\Support\Str;
1313
use Illuminate\Validation\Rules\Password as PasswordRules;
1414
use Illuminate\Validation\ValidationException;
15+
use Statamic\Facades\URL;
1516

1617
/**
1718
* A copy of Illuminate\Auth\ResetsPasswords.
@@ -49,8 +50,10 @@ public function reset(Request $request)
4950
$validator = Validator::make($request->all(), $this->rules(), $this->validationErrorMessages());
5051

5152
if (! $validator->passes()) {
52-
$redirect = $request->has('_error_redirect')
53-
? redirect($request->input('_error_redirect'))
53+
$errorRedirect = $request->input('_error_redirect');
54+
55+
$redirect = $errorRedirect && ! URL::isExternalToApplication($errorRedirect)
56+
? redirect($errorRedirect)
5457
: back();
5558

5659
return $redirect
@@ -173,8 +176,10 @@ protected function sendResetFailedResponse(Request $request, $response)
173176
]);
174177
}
175178

176-
$redirect = $request->has('_error_redirect')
177-
? redirect($request->input('_error_redirect'))
179+
$errorRedirect = $request->input('_error_redirect');
180+
181+
$redirect = $errorRedirect && ! URL::isExternalToApplication($errorRedirect)
182+
? redirect($errorRedirect)
178183
: back();
179184

180185
return $redirect

src/Auth/SendsPasswordResetEmails.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Http\Request;
77
use Illuminate\Support\Facades\Password;
88
use Illuminate\Validation\ValidationException;
9+
use Statamic\Facades\URL;
910

1011
/**
1112
* A copy of Illuminate\Auth\SendsPasswordResetEmails.
@@ -85,8 +86,10 @@ protected function sendResetLinkResponse(Request $request, $response)
8586
{
8687
session()->flash('user.forgot_password.success', __(Password::RESET_LINK_SENT));
8788

88-
$redirect = $request->has('_redirect')
89-
? redirect($request->input('_redirect'))
89+
$successRedirect = $request->input('_redirect');
90+
91+
$redirect = $successRedirect && ! URL::isExternalToApplication($successRedirect)
92+
? redirect($successRedirect)
9093
: back();
9194

9295
return $request->wantsJson()
@@ -102,8 +105,10 @@ protected function sendResetLinkResponse(Request $request, $response)
102105
*/
103106
protected function sendResetLinkFailedResponse(Request $request, $response)
104107
{
105-
$redirect = $request->has('_error_redirect')
106-
? redirect($request->input('_error_redirect'))
108+
$errorRedirect = $request->input('_error_redirect');
109+
110+
$redirect = $errorRedirect && ! URL::isExternalToApplication($errorRedirect)
111+
? redirect($errorRedirect)
107112
: back();
108113

109114
if ($request->wantsJson()) {

src/Facades/Endpoint/URL.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,29 @@ public function isExternal($url)
245245
return $isExternal;
246246
}
247247

248+
/**
249+
* Check whether a URL is external to whole Statamic application.
250+
*/
251+
public function isExternalToApplication(?string $url): bool
252+
{
253+
if (Str::startsWith($url, '//')) {
254+
return true;
255+
}
256+
257+
$urlDomain = parse_url($url, PHP_URL_HOST);
258+
$currentRequestDomain = parse_url(url()->to('/'), PHP_URL_HOST);
259+
260+
return $urlDomain
261+
? Site::all()
262+
->map(fn ($site) => parse_url($site->absoluteUrl(), PHP_URL_HOST))
263+
->push($currentRequestDomain)
264+
->filter(fn ($siteDomain) => ! is_null($siteDomain))
265+
->unique()
266+
->filter(fn ($siteDomain) => $siteDomain === $urlDomain)
267+
->isEmpty()
268+
: false;
269+
}
270+
248271
public function clearExternalUrlCache()
249272
{
250273
self::$externalUriCache = [];

src/Http/Controllers/CP/Auth/LoginController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Validation\ValidationException;
88
use Statamic\Auth\ThrottlesLogins;
99
use Statamic\Facades\OAuth;
10+
use Statamic\Facades\URL;
1011
use Statamic\Http\Controllers\CP\CpController;
1112
use Statamic\Http\Middleware\CP\RedirectIfAuthorized;
1213
use Statamic\Support\Str;
@@ -135,7 +136,9 @@ public function logout(Request $request)
135136

136137
$request->session()->regenerateToken();
137138

138-
return redirect($request->redirect ?? '/');
139+
$redirect = $request->redirect ?? '/';
140+
141+
return redirect(URL::isExternalToApplication($redirect) ? '/' : $redirect);
139142
}
140143

141144
protected function getReferrer()

src/Http/Controllers/ForgotPasswordController.php

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Statamic\Auth\Passwords\PasswordReset;
88
use Statamic\Auth\SendsPasswordResetEmails;
99
use Statamic\Exceptions\ValidationException;
10-
use Statamic\Facades\Site;
1110
use Statamic\Facades\URL;
1211
use Statamic\Http\Middleware\RedirectIfAuthenticated;
1312

@@ -32,26 +31,11 @@ public function showLinkRequestForm()
3231
public function sendResetLinkEmail(Request $request)
3332
{
3433
if ($url = $request->_reset_url) {
35-
$url = URL::makeAbsolute($url);
36-
37-
$urlDomain = parse_url($url, PHP_URL_HOST);
38-
$currentRequestDomain = parse_url(url()->to('/'), PHP_URL_HOST);
39-
40-
$isExternal = $urlDomain
41-
? Site::all()
42-
->map(fn ($site) => parse_url($site->absoluteUrl(), PHP_URL_HOST))
43-
->push($currentRequestDomain)
44-
->filter(fn ($siteDomain) => ! is_null($siteDomain))
45-
->unique()
46-
->filter(fn ($siteDomain) => $siteDomain === $urlDomain)
47-
->isEmpty()
48-
: false;
49-
50-
throw_if($isExternal, ValidationException::withMessages([
34+
throw_if(URL::isExternalToApplication($url), ValidationException::withMessages([
5135
'_reset_url' => trans('validation.url', ['attribute' => '_reset_url']),
5236
]));
5337

54-
PasswordReset::resetFormUrl($url);
38+
PasswordReset::resetFormUrl(URL::makeAbsolute($url));
5539
}
5640

5741
return $this->traitSendResetLinkEmail($request);

src/Http/Controllers/OAuthController.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Laravel\Socialite\Two\InvalidStateException;
99
use Statamic\Exceptions\NotFoundHttpException;
1010
use Statamic\Facades\OAuth;
11+
use Statamic\Facades\URL;
1112
use Statamic\Support\Arr;
1213
use Statamic\Support\Str;
1314

@@ -77,7 +78,9 @@ protected function successRedirectUrl()
7778

7879
parse_str($query, $query);
7980

80-
return Arr::get($query, 'redirect', $default);
81+
$redirect = Arr::get($query, 'redirect', $default);
82+
83+
return URL::isExternalToApplication($redirect) ? $default : $redirect;
8184
}
8285

8386
protected function unauthorizedRedirectUrl()

src/Http/Controllers/ResetPasswordController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Statamic\Auth\Passwords\PasswordReset;
99
use Statamic\Auth\ResetsPasswords;
1010
use Statamic\Contracts\Auth\User;
11+
use Statamic\Facades\URL;
1112
use Statamic\Http\Middleware\CP\RedirectIfAuthorized;
1213

1314
class ResetPasswordController extends Controller
@@ -41,7 +42,11 @@ protected function resetFormTitle()
4142

4243
public function redirectPath()
4344
{
44-
return request('redirect') ?? route('statamic.site');
45+
$redirect = request('redirect');
46+
47+
return $redirect && ! URL::isExternalToApplication($redirect)
48+
? $redirect
49+
: route('statamic.site');
4550
}
4651

4752
protected function setUserPassword($user, $password)

src/Http/Controllers/User/LoginController.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Support\Facades\Auth;
66
use Statamic\Auth\ThrottlesLogins;
7+
use Statamic\Facades\URL;
78
use Statamic\Http\Controllers\Controller;
89
use Statamic\Http\Requests\UserLoginRequest;
910

@@ -20,12 +21,18 @@ public function login(UserLoginRequest $request)
2021
}
2122

2223
if (Auth::attempt($request->only('email', 'password'), $request->has('remember'))) {
23-
return redirect($request->input('_redirect', '/'))->withSuccess(__('Login successful.'));
24+
$redirect = $request->input('_redirect', '/');
25+
26+
return redirect(URL::isExternalToApplication($redirect) ? '/' : $redirect)->withSuccess(__('Login successful.'));
2427
}
2528

2629
$this->incrementLoginAttempts($request);
2730

28-
$errorResponse = $request->has('_error_redirect') ? redirect($request->input('_error_redirect')) : back();
31+
$errorRedirect = $request->input('_error_redirect');
32+
33+
$errorResponse = $errorRedirect && ! URL::isExternalToApplication($errorRedirect)
34+
? redirect($errorRedirect)
35+
: back();
2936

3037
return $errorResponse->withInput()->withErrors(__('Invalid credentials.'));
3138
}
@@ -34,7 +41,9 @@ public function logout()
3441
{
3542
Auth::logout();
3643

37-
return redirect(request()->get('redirect', '/'));
44+
$redirect = request()->get('redirect', '/');
45+
46+
return redirect(URL::isExternalToApplication($redirect) ? '/' : $redirect);
3847
}
3948

4049
protected function username()

src/Http/Controllers/User/PasswordController.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Statamic\Http\Controllers\User;
44

55
use Illuminate\Support\Facades\Password;
6+
use Statamic\Facades\URL;
67
use Statamic\Facades\User;
78
use Statamic\Http\Requests\UserPasswordRequest;
89

@@ -21,7 +22,8 @@ public function __invoke(UserPasswordRequest $request)
2122

2223
private function successfulResponse()
2324
{
24-
$response = request()->has('_redirect') ? redirect(request()->get('_redirect')) : back();
25+
$redirect = request()->get('_redirect');
26+
$response = $redirect && ! URL::isExternalToApplication($redirect) ? redirect($redirect) : back();
2527

2628
if (request()->ajax() || request()->wantsJson()) {
2729
return response([

src/Http/Controllers/User/ProfileController.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Statamic\Http\Controllers\User;
44

5+
use Statamic\Facades\URL;
56
use Statamic\Facades\User;
67
use Statamic\Http\Requests\UserProfileRequest;
78

@@ -26,7 +27,8 @@ public function __invoke(UserProfileRequest $request)
2627

2728
private function successfulResponse()
2829
{
29-
$response = request()->has('_redirect') ? redirect(request()->get('_redirect')) : back();
30+
$redirect = request()->get('_redirect');
31+
$response = $redirect && ! URL::isExternalToApplication($redirect) ? redirect($redirect) : back();
3032

3133
if (request()->ajax() || request()->wantsJson()) {
3234
return response([

0 commit comments

Comments
 (0)