CVE-2021-3851 (published 2021-10-19T13:15Z)

firefly-iii is vulnerable to URL Redirection to Untrusted Site

Severity: MEDIUM
CVSS v2 base score: 4.9 (AV:N/AC:M/Au:S/C:P/I:P/A:N)
CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/firefly-iii/firefly-iii
commit_details: repo_url: https://github.com/firefly-iii/firefly-iii
author: James Cole
author_date: 2021-10-02 13:28:56+02:00
msg: Fix unsecure redirect code.
repository: repo_url: https://github.com/firefly-iii/firefly-iii
repo_name: firefly-iii/firefly-iii
description: Firefly III: a personal finances manager
repo_language: PHP
owner: firefly-iii
file_changes: filename: Handler.php
change_type: ModificationType.MODIFY
diff: @@ -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;
+    }
 }

programming_language: PHP
filename: StartFireflySession.php
change_type: ModificationType.MODIFY
diff: @@ -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()) {

programming_language: PHP
filename: UserNavigation.php
change_type: ModificationType.MODIFY
diff: @@ -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);
 

programming_language: PHP