diff --git a/src/Core/Form.php b/src/Core/Form.php index e674f59870..a1eaa877b2 100644 --- a/src/Core/Form.php +++ b/src/Core/Form.php @@ -47,7 +47,6 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface as SymfFormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * This class converts our own form representation to Symfony's @@ -221,18 +220,14 @@ abstract class Form public static function forceRedirect(SymfFormInterface $form, Request $request): RedirectResponse { $next = $form->get('_next')->getData(); - try { - if ($pos = mb_strrpos($next, '#')) { - $fragment = mb_substr($next, $pos); - $next = mb_substr($next, 0, $pos); - } - Router::match($next); - return new RedirectResponse(url: $next . ($fragment ?? '')); - } catch (ResourceNotFoundException $e) { + $url = Router::sanitizeLocalURL($next, ['next' => false]); + if (!\is_null($url)) { + return new RedirectResponse(url: $url); + } else { $user = Common::user(); $user_id = !\is_null($user) ? $user->getId() : '(not logged in)'; Log::warning("Suspicious activity: User with ID {$user_id} submitted a form where the `_next` parameter is not a valid local URL ({$next})"); - throw new ClientException(_m('Invalid form submission'), $e); + throw new ClientException(_m('Invalid form submission'), previous: $e); } } } diff --git a/src/Core/Router/Router.php b/src/Core/Router/Router.php index 59104d836d..c17dfa4867 100644 --- a/src/Core/Router/Router.php +++ b/src/Core/Router/Router.php @@ -33,6 +33,8 @@ declare(strict_types = 1); namespace App\Core\Router; use App\Core\Log; +use App\Util\Common; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Router as SymfonyRouter; @@ -89,6 +91,26 @@ abstract class Router return self::$router->generate($id, $args, $type); } + public static function sanitizeLocalURL(string $url, array $unset_query_args = []): ?string + { + try { + $parts = parse_url($url); + if ($parts === false || (isset($parts['host']) && $parts['host'] !== Common::config('site', 'server'))) { + return null; + } + self::match($parts['path']); + if ($unset_query_args !== [] && isset($parts['query'])) { + $args = []; + parse_str($parts['query'], $args); + $args = array_diff_key($args, $unset_query_args); + $parts['query'] = http_build_query($args); + } + return $parts['path'] . (empty($parts['query']) ? '' : ('?' . $parts['query'])) . (empty($parts['fragment']) ? '' : ('#' . $parts['fragment'])); + } catch (ResourceNotFoundException) { + return null; + } + } + /** * function match($url) throws Symfony\Component\Routing\Exception\ResourceNotFoundException */ diff --git a/src/Util/Common.php b/src/Util/Common.php index 341b2b7e42..abf520f4d5 100644 --- a/src/Util/Common.php +++ b/src/Util/Common.php @@ -148,11 +148,10 @@ abstract class Common return self::ensureLoggedIn()->getId(); } - public static function ensureLoggedIn(): LocalUser + public static function ensureLoggedIn(?Request $request = null): LocalUser { - if (($user = self::user()) == null) { - throw new NoLoggedInUser(self::getRequest()); - // TODO Maybe redirect to login page and back + if (\is_null($user = self::user())) { + throw new NoLoggedInUser($request ?? self::getRequest()); } else { return $user; }