diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 0cada87b22c..d9138ffef74 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -56,6 +56,7 @@ use OCP\IUser; use OCP\IUserSession; use OCP\Notification\IManager as INotificationManager; +use OCP\Security\Bruteforce\IThrottler; use Psr\Log\LoggerInterface; class PageController extends Controller { @@ -73,6 +74,7 @@ class PageController extends Controller { private INotificationManager $notificationManager; private IAppManager $appManager; private IRootFolder $rootFolder; + private IThrottler $throttler; public function __construct(string $appName, IRequest $request, @@ -90,6 +92,7 @@ public function __construct(string $appName, IInitialState $initialState, ICacheFactory $memcacheFactory, IRootFolder $rootFolder, + IThrottler $throttler, Config $talkConfig, IConfig $serverConfig) { parent::__construct($appName, $request); @@ -107,6 +110,7 @@ public function __construct(string $appName, $this->initialState = $initialState; $this->memcacheFactory = $memcacheFactory; $this->rootFolder = $rootFolder; + $this->throttler = $throttler; $this->talkConfig = $talkConfig; $this->serverConfig = $serverConfig; } @@ -129,6 +133,7 @@ public function showCall(string $token): Response { * @PublicPage * @NoCSRFRequired * @UseSession + * @BruteForceProtection(action=talkRoomPassword) * * @param string $token * @param string $password @@ -177,6 +182,7 @@ public function index(string $token = '', string $callUser = '', string $passwor return $this->guestEnterRoom($token, $password); } + $throttle = false; if ($token !== '') { $room = null; try { @@ -205,6 +211,7 @@ public function index(string $token = '', string $callUser = '', string $passwor } catch (RoomNotFoundException $e) { // Room not found, redirect to main page $token = ''; + $throttle = true; } if ($room instanceof Room && $room->hasPassword()) { @@ -224,15 +231,22 @@ public function index(string $token = '', string $callUser = '', string $passwor if ($passwordVerification['result']) { $this->talkSession->renewSessionId(); $this->talkSession->setPasswordForRoom($token, $password); + $this->throttler->resetDelay($this->request->getRemoteAddress(), 'talkRoomPassword', ['token' => $token]); } else { $this->talkSession->removePasswordForRoom($token); + $showBruteForceWarning = $this->throttler->getDelay($this->request->getRemoteAddress(), 'talkRoomPassword') > 5000; + if ($passwordVerification['url'] === '') { - return new TemplateResponse($this->appName, 'authenticate', [ + $response = new TemplateResponse($this->appName, 'authenticate', [ 'wrongpw' => $password !== '', + 'showBruteForceWarning' => $showBruteForceWarning, ], 'guest'); + } else { + $response = new RedirectResponse($passwordVerification['url']); } - return new RedirectResponse($passwordVerification['url']); + $response->throttle(['token' => $token]); + return $response; } } } @@ -268,6 +282,10 @@ public function index(string $token = '', string $callUser = '', string $passwor $csp->addAllowedConnectDomain("'self'"); $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); $response->setContentSecurityPolicy($csp); + if ($throttle) { + // Logged-in user tried to access a chat they can not access + $response->throttle(); + } return $response; } @@ -288,9 +306,11 @@ protected function guestEnterRoom(string $token, string $password): Response { if ($token) { $redirectUrl = $this->url->linkToRoute('spreed.Page.showCall', ['token' => $token]); } - return new RedirectResponse($this->url->linkToRoute('core.login.showLoginForm', [ + $response = new RedirectResponse($this->url->linkToRoute('core.login.showLoginForm', [ 'redirect_url' => $redirectUrl, ])); + $response->throttle(); + return $response; } if ($room->hasPassword()) { @@ -300,15 +320,21 @@ protected function guestEnterRoom(string $token, string $password): Response { if ($passwordVerification['result']) { $this->talkSession->renewSessionId(); $this->talkSession->setPasswordForRoom($token, $password); + $this->throttler->resetDelay($this->request->getRemoteAddress(), 'talkRoomPassword', ['token' => $token]); } else { $this->talkSession->removePasswordForRoom($token); + $showBruteForceWarning = $this->throttler->getDelay($this->request->getRemoteAddress(), 'talkRoomPassword') > 5000; + if ($passwordVerification['url'] === '') { - return new TemplateResponse($this->appName, 'authenticate', [ + $response = new TemplateResponse($this->appName, 'authenticate', [ 'wrongpw' => $password !== '', + 'showBruteForceWarning' => $showBruteForceWarning, ], 'guest'); + } else { + $response = new RedirectResponse($passwordVerification['url']); } - - return new RedirectResponse($passwordVerification['url']); + $response->throttle(['token' => $token]); + return $response; } } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 40c457de339..f8e3eb55a7f 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -260,6 +260,7 @@ public function getListedRooms(string $searchTerm = ''): DataResponse { /** * @PublicPage + * @BruteForceProtection(action=sipBridgeSecret) * * @param string $token * @return DataResponse @@ -268,7 +269,9 @@ public function getSingleRoom(string $token): DataResponse { try { $isSIPBridgeRequest = $this->validateSIPBridgeRequest($token); } catch (UnauthorizedException $e) { - return new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response->throttle(); + return $response; } // The SIP bridge only needs room details (public, sip enabled, lobby state, etc) @@ -1330,6 +1333,7 @@ public function setPassword(string $password): DataResponse { /** * @PublicPage * @UseSession + * @BruteForceProtection(action=talkRoomPassword) * * @param string $token * @param string $password @@ -1389,9 +1393,13 @@ public function joinRoom(string $token, string $password = '', bool $force = tru $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant); } } catch (InvalidPasswordException $e) { - return new DataResponse([], Http::STATUS_FORBIDDEN); + $response = new DataResponse([], Http::STATUS_FORBIDDEN); + $response->throttle(['token' => $token]); + return $response; } catch (UnauthorizedException $e) { - return new DataResponse([], Http::STATUS_NOT_FOUND); + $response = new DataResponse([], Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $token]); + return $response; } $this->session->removePasswordForRoom($token); @@ -1407,6 +1415,7 @@ public function joinRoom(string $token, string $password = '', bool $force = tru /** * @PublicPage * @RequireRoom + * @BruteForceProtection(action=sipBridgeSecret) * * @param string $pin * @return DataResponse @@ -1414,10 +1423,14 @@ public function joinRoom(string $token, string $password = '', bool $force = tru public function getParticipantByDialInPin(string $pin): DataResponse { try { if (!$this->validateSIPBridgeRequest($this->room->getToken())) { - return new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response->throttle(); + return $response; } } catch (UnauthorizedException $e) { - return new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response->throttle(); + return $response; } try { diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 4f9bc62ba5c..49bce4bed69 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -461,19 +461,22 @@ protected function getInputStream(): string { * https://nextcloud-spreed-signaling.readthedocs.io/en/latest/standalone-signaling-api-v1/#backend-requests * * @PublicPage + * @BruteForceProtection(action=signalingSecret) * * @return DataResponse */ public function backend(): DataResponse { $json = $this->getInputStream(); if (!$this->validateBackendRequest($json)) { - return new DataResponse([ + $response = new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'invalid_request', 'message' => 'The request could not be authenticated.', ], ]); + $response->throttle(); + return $response; } $message = json_decode($json, true); diff --git a/templates/authenticate.php b/templates/authenticate.php index f62042e1b9c..ba7d5e7a695 100644 --- a/templates/authenticate.php +++ b/templates/authenticate.php @@ -7,7 +7,14 @@