CVE-2024-21641 (published 2024-01-05T21:15Z)

Flarum is open source discussion platform software. Prior to version 1.8.5, the Flarum `/logout` route includes a redirect parameter that allows any third party to redirect users from a (trusted) domain of the Flarum installation to redirect to any link. For logged-in users, the logout must be confirmed. Guests are immediately redirected. This could be used by spammers to redirect to a web address using a trusted domain of a running Flarum installation. The vulnerability has been fixed and published as flarum/core v1.8.5. As a workaround, some extensions modifying the logout route can remedy this issue if their implementation is safe.

CVSS v3 base score: 4.7, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/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/flarum/flarum-core
commit_details: repo_url: https://github.com/flarum/flarum-core
author: IanM
author_date: 2024-01-05 15:33:10+00:00
msg: [1.x] fix: Logout controller allows open redirects (#3948)

* fix: prevent open redirects on logout controller

* use clearer config key

* cast url as string, reinstate guest redirect

* clean up a little

* simplify

* return Uri

* resolve ternary always true

* simplify some more

* remove extra newline

* handle malformed uri

* chore: requested changes
repository: repo_url: https://github.com/flarum/flarum-core
repo_name: flarum/flarum-core
description: [READ ONLY] Subtree split of Flarum framework core.
repo_language: PHP
owner: flarum
file_changes: filename: LogOutController.php
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,7 @@
 
 namespace Flarum\Forum\Controller;
 
+use Flarum\Foundation\Config;
 use Flarum\Http\Exception\TokenMismatchException;
 use Flarum\Http\Rememberer;
 use Flarum\Http\RequestUtil;
@@ -20,6 +21,7 @@ use Illuminate\Contracts\View\Factory;
 use Illuminate\Support\Arr;
 use Laminas\Diactoros\Response\HtmlResponse;
 use Laminas\Diactoros\Response\RedirectResponse;
+use Laminas\Diactoros\Uri;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Http\Server\RequestHandlerInterface;
@@ -51,25 +53,33 @@ class LogOutController implements RequestHandlerInterface
      */
     protected $url;
 
+    /**
+     * @var Config
+     */
+    protected $config;
+
     /**
      * @param Dispatcher $events
      * @param SessionAuthenticator $authenticator
      * @param Rememberer $rememberer
      * @param Factory $view
      * @param UrlGenerator $url
+     * @param Config $config
      */
     public function __construct(
         Dispatcher $events,
         SessionAuthenticator $authenticator,
         Rememberer $rememberer,
         Factory $view,
-        UrlGenerator $url
+        UrlGenerator $url,
+        Config $config
     ) {
         $this->events = $events;
         $this->authenticator = $authenticator;
         $this->rememberer = $rememberer;
         $this->view = $view;
         $this->url = $url;
+        $this->config = $config;
     }
 
     /**
@@ -81,12 +91,14 @@ class LogOutController implements RequestHandlerInterface
     {
         $session = $request->getAttribute('session');
         $actor = RequestUtil::getActor($request);
+        $base = $this->url->to('forum')->base();
 
-        $url = Arr::get($request->getQueryParams(), 'return', $this->url->to('forum')->base());
+        $returnUrl = Arr::get($request->getQueryParams(), 'return');
+        $return = $this->sanitizeReturnUrl((string) $returnUrl, $base);
 
-        // If there is no user logged in, return to the index.
+        // If there is no user logged in, return to the index or the return url if it's set.
         if ($actor->isGuest()) {
-            return new RedirectResponse($url);
+            return new RedirectResponse($return);
         }
 
         // If a valid CSRF token hasn't been provided, show a view which will
@@ -94,16 +106,14 @@ class LogOutController implements RequestHandlerInterface
         $csrfToken = $session->token();
 
         if (Arr::get($request->getQueryParams(), 'token') !== $csrfToken) {
-            $return = Arr::get($request->getQueryParams(), 'return');
-
             $view = $this->view->make('flarum.forum::log-out')
-                ->with('url', $this->url->to('forum')->route('logout').'?token='.$csrfToken.($return ? '&return='.urlencode($return) : ''));
+                ->with('url', $this->url->to('forum')->route('logout') . '?token=' . $csrfToken . ($returnUrl ? '&return=' . urlencode($return) : ''));
 
             return new HtmlResponse($view->render());
         }
 
         $accessToken = $session->get('access_token');
-        $response = new RedirectResponse($url);
+        $response = new RedirectResponse($return);
 
         $this->authenticator->logOut($session);
 
@@ -113,4 +123,33 @@ class LogOutController implements RequestHandlerInterface
 
         return $this->rememberer->forget($response);
     }
+
+    protected function sanitizeReturnUrl(string $url, string $base): Uri
+    {
+        if (empty($url)) {
+            return new Uri($base);
+        }
+
+        try {
+            $parsedUrl = new Uri($url);
+        } catch (\InvalidArgumentException $e) {
+            return new Uri($base);
+        }
+
+        if (in_array($parsedUrl->getHost(), $this->getAllowedRedirectDomains())) {
+            return $parsedUrl;
+        }
+
+        return new Uri($base);
+    }
+
+    protected function getAllowedRedirectDomains(): array
+    {
+        $forumUri = $this->config->url();
+
+        return array_merge(
+            [$forumUri->getHost()],
+            $this->config->offsetGet('redirectDomains') ?? []
+        );
+    }
 }

programming_language: PHP