From 8662dfa4c0f71efef61c31dc015c6f723db8318d Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 2 Oct 2021 13:28:56 +0200 Subject: [PATCH] Fix unsecure redirect code. --- app/Exceptions/Handler.php | 57 +++++++++++++++++++ app/Http/Middleware/StartFireflySession.php | 1 - .../Http/Controllers/UserNavigation.php | 2 +- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f6fdbd62442..ba68632bd0d 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -29,8 +29,11 @@ use ErrorException; use FireflyIII\Jobs\MailError; use Illuminate\Auth\AuthenticationException; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Routing\Redirector; use Illuminate\Session\TokenMismatchException; use Illuminate\Support\Arr; use Illuminate\Validation\ValidationException as LaravelValidationException; @@ -187,4 +190,58 @@ private function shouldntReportLocal(Throwable $e): bool ) ); } + + /** + * Convert a validation exception into a response. + * + * @param Request $request + * @param LaravelValidationException $exception + * + * @return Application|RedirectResponse|Redirector + */ + protected function invalid($request, LaravelValidationException $exception): Application|RedirectResponse|Redirector + { + // protect against open redirect when submitting invalid forms. + $previous = $this->getPreviousUrl(); + $redirect = $this->getRedirectUrl($exception); + + return redirect($redirect ?? $previous) + ->withInput(Arr::except($request->input(), $this->dontFlash)) + ->withErrors($exception->errors(), $request->input('_error_bag', $exception->errorBag)); + } + + /** + * Only return the previousUrl() if it is a valid URL. Return default redirect otherwise. + * + * @return string + */ + private function getPreviousUrl(): string + { + $safe = route('index'); + $previous = url()->previous(); + $previousHost = parse_url($previous, PHP_URL_HOST); + $safeHost = parse_url($safe, PHP_URL_HOST); + + return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe; + } + + /** + * Only return the redirectTo property from the exception if it is a valid URL. Return NULL otherwise. + * + * @param LaravelValidationException $exception + * + * @return string|null + */ + private function getRedirectUrl(LaravelValidationException $exception): ?string + { + if (null === $exception->redirectTo) { + return null; + } + $safe = route('index'); + $previous = $exception->redirectTo; + $previousHost = parse_url($previous, PHP_URL_HOST); + $safeHost = parse_url($safe, PHP_URL_HOST); + + return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe; + } } diff --git a/app/Http/Middleware/StartFireflySession.php b/app/Http/Middleware/StartFireflySession.php index 0cd5de179e1..eaf9b8ff9e3 100644 --- a/app/Http/Middleware/StartFireflySession.php +++ b/app/Http/Middleware/StartFireflySession.php @@ -45,7 +45,6 @@ protected function storeCurrentUrl(Request $request, $session): void $url = $request->fullUrl(); $forbiddenWords = strpos($url, 'offline') || strpos($url, 'jscript') || strpos($url, 'delete') || strpos($url, '/login') || strpos($url, '/json') || strpos($url, 'serviceworker') || strpos($url, '/attachments/view'); - // also stop remembering "delete" URL's. if (false === $forbiddenWords && 'GET' === $request->method() && !$request->ajax()) { diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php index 3b0eafaabbe..58192ac75db 100644 --- a/app/Support/Http/Controllers/UserNavigation.php +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -183,7 +183,7 @@ final protected function rememberPreviousUri(string $identifier): ?string // get host of previous URL: $previous = parse_url($return, PHP_URL_HOST); - if ($default === $previous && (null === $errors || (0 === $errors->count())) && !Str::contains($return, $forbidden)) { + if (null !== $previous && $default === $previous && (null === $errors || (0 === $errors->count())) && !Str::contains($return, $forbidden)) { Log::debug(sprintf('Saving URL %s under key %s', $return, $identifier)); session()->put($identifier, $return);