diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index ddbf4c4..064aa6b 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -15,6 +15,12 @@ framework: storage_factory_id: session.storage.factory.native save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' + rate_limiter: + invite_code_join: + policy: 'sliding_window' + limit: 10 + interval: '1 minute' + when@prod: framework: session: diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b1e65cf..2f9ef46 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -22,6 +22,9 @@ security: enable_csrf: true username_parameter: username password_parameter: password + login_throttling: + max_attempts: 5 + interval: '15 minutes' logout: path: app_logout # where to redirect after logout diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index 4134c9e..956eb07 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -22,6 +22,8 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\Target; +use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; final class GameController extends AbstractController { @@ -39,13 +41,20 @@ final class GameController extends AbstractController GameRepository $gameRepository, SessionRepository $sessionRepository, GameDashboardService $dashboardService, - Security $security + Security $security, + #[Target('invite_code_join')] + RateLimiterFactoryInterface $inviteCodeJoinLimiter ): Response { $user = $security->getUser(); $isAdmin = $this->isGranted('ROLE_ADMIN'); if ($request->isMethod('POST')) { if ($request->request->has('create_session')) { + if (!$this->isCsrfTokenValid('create_session', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_dashboard'); + } + $gameId = $request->request->get('game_id'); $game = $gameRepository->find($gameId); @@ -55,6 +64,17 @@ final class GameController extends AbstractController } } } elseif ($request->request->has('join_session')) { + if (!$this->isCsrfTokenValid('join_session', $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_dashboard'); + } + + $limiter = $inviteCodeJoinLimiter->create($user->getUserIdentifier()); + if (!$limiter->consume(1)->isAccepted()) { + $this->addFlash('error', 'Too many attempts. Please wait a moment and try again.'); + return $this->redirectToRoute('game_dashboard'); + } + $inviteCode = $request->request->get('invite_code'); if ($dashboardService->joinSession($inviteCode, $user)) { $this->addFlash('success', 'Joined session successfully!'); @@ -70,6 +90,11 @@ final class GameController extends AbstractController return $this->redirectToRoute('game_dashboard'); } + if (!$this->isCsrfTokenValid('create_invite_' . $session->getId(), $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_dashboard'); + } + $inviteCode = $dashboardService->generateInviteCode($session, $user, $isAdmin); if ($inviteCode) { $this->addFlash('success', 'Invite link created: ' . $inviteCode); @@ -79,6 +104,11 @@ final class GameController extends AbstractController $session = $sessionRepository->find($sessionId); if ($session) { + if (!$this->isCsrfTokenValid('leave_session_' . $session->getId(), $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_dashboard'); + } + if ($dashboardService->leaveSession($session, $user)) { $this->addFlash('success', 'Left session successfully.'); } else { @@ -90,6 +120,11 @@ final class GameController extends AbstractController $session = $sessionRepository->find($sessionId); if ($session) { + if (!$this->isCsrfTokenValid('start_session_' . $session->getId(), $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + return $this->redirectToRoute('game_dashboard'); + } + if ($dashboardService->startSession($session)) { $this->addFlash('success', 'Session started! Screens have been assigned.'); } else { @@ -127,7 +162,9 @@ final class GameController extends AbstractController $player = $playerRepository->findOneBy(['session' => $session, 'user' => $user]); if ($request->isMethod('POST') && $request->request->has('toggle_ready')) { - if (!$user->isVerified()) { + if (!$this->isCsrfTokenValid('toggle_ready_' . $session->getId(), $request->request->get('_token'))) { + $this->addFlash('error', 'Invalid CSRF token.'); + } elseif (!$user->isVerified()) { $this->addFlash('error', 'You must verify your email address before you can mark yourself as ready.'); } else { $dashboardService->toggleReady($session, $user); @@ -137,12 +174,16 @@ final class GameController extends AbstractController } if ($request->isMethod('POST') && $request->request->has('expire_ready')) { - $dashboardService->expireOwnReadyIfDue($session, $user); + if ($this->isCsrfTokenValid('expire_ready_' . $session->getId(), $request->request->get('_token'))) { + $dashboardService->expireOwnReadyIfDue($session, $user); + } return $this->redirectToRoute('game', ['session' => $session->getId()]); } if ($request->isMethod('POST') && $request->request->has('send_message')) { - $dashboardService->postLobbyMessage($session, $user, (string) $request->request->get('content', '')); + if ($this->isCsrfTokenValid('send_message_' . $session->getId(), $request->request->get('_token'))) { + $dashboardService->postLobbyMessage($session, $user, (string) $request->request->get('content', '')); + } return $this->redirectToRoute('game', ['session' => $session->getId()]); } @@ -215,7 +256,7 @@ final class GameController extends AbstractController $user = $security->getUser(); $player = $playerRepository->findOneBy(['session' => $session, 'user' => $user]); - if ($request->isMethod('POST')) { + if ($request->isMethod('POST') && $this->isCsrfTokenValid('game_feedback_' . $session->getId(), $request->request->get('_token'))) { $difficulty = $request->request->get('difficulty'); $entertaining = $request->request->get('entertaining'); $theme = $request->request->get('theme'); @@ -247,7 +288,7 @@ final class GameController extends AbstractController $user = $security->getUser(); $player = $playerRepository->findOneBy(['session' => $session, 'user' => $user]); - if ($request->isMethod('POST')) { + if ($request->isMethod('POST') && $this->isCsrfTokenValid('game_feedback_' . $session->getId(), $request->request->get('_token'))) { $difficulty = $request->request->get('difficulty'); $entertaining = $request->request->get('entertaining'); $theme = $request->request->get('theme'); diff --git a/templates/game/dashboard.html.twig b/templates/game/dashboard.html.twig index 9b73fd7..c81804a 100644 --- a/templates/game/dashboard.html.twig +++ b/templates/game/dashboard.html.twig @@ -17,6 +17,7 @@
{% if availableGames is not empty %}
+
@@ -81,6 +83,7 @@ {{ inviteCode }} {% else %}
+
@@ -91,12 +94,14 @@ {% if session.status.value == 'created' %} {% if session.players|length >= session.game.numberOfPlayers %}
+
{% endif %} {% if session.timer == 0 %}
+
diff --git a/templates/game/lobby.html.twig b/templates/game/lobby.html.twig index 9934258..25f7d28 100644 --- a/templates/game/lobby.html.twig +++ b/templates/game/lobby.html.twig @@ -29,6 +29,7 @@ {% if session.players|length >= session.game.numberOfPlayers %}
+
@@ -71,6 +72,7 @@ {% if player %}
+
diff --git a/templates/game/lost.html.twig b/templates/game/lost.html.twig index 4bb7eca..32a42e1 100644 --- a/templates/game/lost.html.twig +++ b/templates/game/lost.html.twig @@ -78,6 +78,7 @@
Feedback
+
diff --git a/templates/game/waiting.html.twig b/templates/game/waiting.html.twig index 88c43bc..3db4102 100644 --- a/templates/game/waiting.html.twig +++ b/templates/game/waiting.html.twig @@ -63,6 +63,7 @@
{% endif %} +
@@ -87,6 +88,7 @@
+
diff --git a/templates/game/won.html.twig b/templates/game/won.html.twig index d844969..20eb5f7 100644 --- a/templates/game/won.html.twig +++ b/templates/game/won.html.twig @@ -78,6 +78,7 @@