From 8aeb1ccd857d2e181eb0f0fa75f6654fe7bce21d Mon Sep 17 00:00:00 2001 From: adnanmula Date: Wed, 19 Nov 2025 20:29:18 +0100 Subject: [PATCH 1/4] add csrf protection --- config/packages/framework.yaml | 4 ++-- config/packages/security.yaml | 3 +++ config/routes.yaml | 2 +- .../Competition/CompetitionController.php | 20 +++++++++++++++++++ .../CreateCompetitionGameController.php | 4 ++++ .../Competition/competition_detail.html.twig | 7 ++++++- .../Competition/create_competition.html.twig | 2 ++ .../Alliance/GenerateAlliancesController.php | 8 ++++++-- .../Alliance/generate_alliances.html.twig | 1 + .../Keyforge/Deck/Detail/deck.html.twig | 10 ++++++++++ .../Deck/Import/ImportDeckController.php | 4 ++++ .../Deck/Import/import_deck.html.twig | 8 ++++++-- .../UpdateNotes/UpdateDeckNotesController.php | 4 ++++ .../UpdateDeckOwnershipController.php | 8 ++++++++ .../Game/Create/CreateGameController.php | 4 ++++ .../Game/Create/create_game.html.twig | 2 ++ .../Game/Detail/GameAnalyzeController.php | 4 ++++ .../Game/Detail/game_analyze.html.twig | 1 + .../Keyforge/Shared/keyforge_base.html.twig | 8 +++++++- .../Keyforge/Tag/AssignTagController.php | 4 ++++ .../Keyforge/Tag/CreateTagController.php | 4 ++++ .../Keyforge/Tag/RemoveTagController.php | 4 ++++ .../Shared/Admin/AdminAccountsController.php | 4 ++++ .../Controller/Shared/Admin/admin.html.twig | 2 ++ .../Controller/Shared/Login/login.html.twig | 2 ++ .../Shared/Login/register.html.twig | 2 ++ .../Shared/User/UserFriendsController.php | 6 ++++++ .../User/UserNotificationsController.php | 8 ++++++++ .../Shared/User/UserSettingsController.php | 4 ++++ .../Shared/User/user_friends.html.twig | 3 +++ .../Shared/User/user_pending_games.html.twig | 2 ++ .../Shared/User/user_settings.html.twig | 1 + 32 files changed, 141 insertions(+), 9 deletions(-) diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index e2d99e0c..5de6ece4 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -1,7 +1,7 @@ framework: secret: '%env(APP_SECRET)%' - #csrf_protection: true - #http_method_override: true + csrf_protection: true + http_method_override: false # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 48fb781f..ab40306e 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -16,8 +16,11 @@ security: form_login: login_path: login check_path: login + enable_csrf: true logout: path: logout + enable_csrf: true + csrf_token_id: authenticate_logout access_control: - { path: ^/games/new, roles: ROLE_KEYFORGE } diff --git a/config/routes.yaml b/config/routes.yaml index 771182ee..e3661998 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -10,7 +10,7 @@ login: logout: path: /logout - methods: [GET] + methods: [POST] user_friends: path: /user/friends diff --git a/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php b/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php index d281767a..0f77d058 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php +++ b/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php @@ -32,6 +32,10 @@ public function create(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { + if (false === $this->isCsrfTokenValid('keyforge_competition_create', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + try { $this->bus->dispatch(new CreateCompetitionCommand( $request->request->get('name'), @@ -77,6 +81,10 @@ public function start(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_competition_start', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new StartCompetitionCommand( $request->get('competitionId'), $request->get('date', new \DateTimeImmutable()->format('Y-m-d')), @@ -89,6 +97,10 @@ public function finish(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_competition_game_finish', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new FinishCompetitionCommand( $request->get('competitionId'), $request->get('winnerId'), @@ -102,6 +114,10 @@ public function join(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_competition_join', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new JoinCompetitionCommand( $request->get('id'), )); @@ -113,6 +129,10 @@ public function leave(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_competition_leave', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new LeaveCompetitionCommand( $request->get('id'), )); diff --git a/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php b/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php index 1dc58892..0fb8d5e7 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php +++ b/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php @@ -14,6 +14,10 @@ public function __invoke(Request $request, string $fixtureId): Response { $this->assertIsLogged(); + if (false === $this->isCsrfTokenValid('keyforge_competition_game_create', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new CreateCompetitionGameCommand( $request->request->get('winner'), $request->request->get('winnerDeck'), diff --git a/src/Entrypoint/Controller/Keyforge/Competition/competition_detail.html.twig b/src/Entrypoint/Controller/Keyforge/Competition/competition_detail.html.twig index 16c3cf95..1fb48a61 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/competition_detail.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Competition/competition_detail.html.twig @@ -332,7 +332,7 @@
- + @@ -384,6 +384,7 @@ 'competition': 'LOCAL_LEAGUE', 'notes': competitionName + ' ' + fixtureRef, 'log': log, + '_csrf_token': '{{ csrf_token('keyforge_competition_game_create') }}', }, }).done(function() { $('#registerCompetitionGame').modal('hide'); @@ -397,6 +398,7 @@ method: 'POST', data: { 'competitionId': $('#hiddenCompetitionId')[0].value, + '_csrf_token': '{{ csrf_token('keyforge_competition_start') }}', }, }).done(function() { location.reload(); @@ -409,6 +411,7 @@ method: 'POST', data: { 'id': '{{ competition.id }}', + '_csrf_token': '{{ csrf_token('keyforge_competition_join') }}', }, }).done(function() { location.reload(); @@ -421,6 +424,7 @@ method: 'POST', data: { 'id': '{{ competition.id }}', + '_csrf_token': '{{ csrf_token('keyforge_competition_leave') }}', }, }).done(function() { location.reload(); @@ -438,6 +442,7 @@ url: '{{ path('keyforge_competition_game_finish') }}', method: 'POST', data: { + '_csrf_token': '{{ csrf_token('keyforge_competition_game_finish') }}', 'competitionId': competitionId, 'winnerId': winnerId, 'date': date, diff --git a/src/Entrypoint/Controller/Keyforge/Competition/create_competition.html.twig b/src/Entrypoint/Controller/Keyforge/Competition/create_competition.html.twig index 173f0786..592f690c 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/create_competition.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Competition/create_competition.html.twig @@ -67,6 +67,8 @@ + +
diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php index 45f6cd89..e93be1d9 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php @@ -21,9 +21,13 @@ public function __invoke(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { - try { - $payload = Json::decode($request->getContent()); + $payload = Json::decode($request->getContent()); + + if (false === $this->isCsrfTokenValid('keyforge_alliance_generate', $payload['_csrf_token'])) { + throw new \Exception('Invalid CSRF token'); + } + try { $result = $this->extractResult( $this->bus->dispatch(new GenerateDeckAlliancesCommand( $payload['decks'], diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/generate_alliances.html.twig b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/generate_alliances.html.twig index 892519f7..d91ccccb 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/generate_alliances.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/generate_alliances.html.twig @@ -218,6 +218,7 @@ extraCard: extraCard, addToMyDecks: $('#addToMyDecks').is(':checked'), addToOwnedDok: $('#addToOwnedDok').is(':checked'), + _csrf_token: '{{ csrf_token('keyforge_alliance_generate') }}', }), }).done(function(data) { if (data.success && data.result.decks.length > 0) { diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Detail/deck.html.twig b/src/Entrypoint/Controller/Keyforge/Deck/Detail/deck.html.twig index c5daf443..b2a517cf 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Detail/deck.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Deck/Detail/deck.html.twig @@ -395,6 +395,7 @@ data: { 'deckId': deckId, 'notes': notes, + '_csrf_token': '{{ csrf_token('keyforge_deck_update_notes') }}', }, }).done(function() { $('#deckNotesInput')[0].innerHTML = notes; @@ -412,6 +413,9 @@ $.post({ url: url, method: 'POST', + data: { + '_csrf_token': '{{ csrf_token('keyforge_ownership_update_add') }}', + }, }).done(function( ) { location.reload(); }).fail(function() { @@ -428,6 +432,9 @@ $.post({ url: url, method: 'DELETE', + data: { + '_csrf_token': '{{ csrf_token('keyforge_ownership_update_remove') }}', + }, }).done(function( ) { location.reload(); }).fail(function() { @@ -445,6 +452,7 @@ method: 'DELETE', data: { 'id': $(this).data('tag'), + '_csrf_token': '{{ csrf_token('keyforge_deck_tag_remove') }}', }, }).done(function( ) { location.reload(); @@ -499,6 +507,7 @@ 'styleText': $('#colorPickerText').val(), 'styleOutline': $('#colorPickerOutline').val(), 'deckId': $('#hiddenDeckId2').val(), + '_csrf_token': '{{ csrf_token('keyforge_deck_tag_create') }}', }, }).done(function() { location.reload(); @@ -514,6 +523,7 @@ data: { 'deckId': $('#hiddenDeckId2').val(), 'tagId': $('#assignTagSelector').val(), + '_csrf_token': '{{ csrf_token('keyforge_deck_tag_add') }}', }, }).done(function() { location.reload(); diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php b/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php index 3657049e..62aac7cf 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php @@ -21,6 +21,10 @@ public function __invoke(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { + if (false === $this->isCsrfTokenValid('keyforge_deck_import', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + try { $deckId = $this->parseDeck($request->request->getString('deck')); $deckType = $request->request->get('deckType'); diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Import/import_deck.html.twig b/src/Entrypoint/Controller/Keyforge/Deck/Import/import_deck.html.twig index 35bf5970..76da89df 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Import/import_deck.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Deck/Import/import_deck.html.twig @@ -38,6 +38,7 @@ +
@@ -56,6 +57,7 @@
+ @@ -77,6 +79,8 @@ + + @@ -96,12 +100,12 @@ + + {% endif %} - - {% endblock %} diff --git a/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php b/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php index dc01db8a..b90a8389 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php @@ -14,6 +14,10 @@ public function __invoke(Request $request): Response { $this->assertIsLogged(); + if (false === $this->isCsrfTokenValid('keyforge_deck_update_notes', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + /** @var User $user */ $user = $this->security->getUser(); diff --git a/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php b/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php index 73f15c37..cb6ec381 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php @@ -39,10 +39,18 @@ public function __invoke(Request $request, string $id): Response } if ($request->getMethod() === Request::METHOD_POST) { + if (false === $this->isCsrfTokenValid('keyforge_ownership_update_add', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->deckRepository->addOwner(Uuid::from($id), $user->id()); } if ($request->getMethod() === Request::METHOD_DELETE) { + if (false === $this->isCsrfTokenValid('keyforge_ownership_update_remove', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->deckRepository->removeOwner(Uuid::from($id), $user->id()); } diff --git a/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php b/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php index 62c9b156..52a7ae70 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php +++ b/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php @@ -25,6 +25,10 @@ public function __invoke(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { + if (false === $this->isCsrfTokenValid('keyforge_game_create', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + try { $this->bus->dispatch(new CreateGameCommand( $request->request->get('winner'), diff --git a/src/Entrypoint/Controller/Keyforge/Game/Create/create_game.html.twig b/src/Entrypoint/Controller/Keyforge/Game/Create/create_game.html.twig index 2a76cdbe..eacb1347 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Create/create_game.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Game/Create/create_game.html.twig @@ -170,6 +170,8 @@
+ +
diff --git a/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php b/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php index 5e1a4574..3da099fd 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php +++ b/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php @@ -41,6 +41,10 @@ public function __invoke(Request $request): Response } try { + if (false === $this->isCsrfTokenValid('keyforge_game_analyze', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $p = new GameLogParser(); $parsedLog = $p->execute($log); diff --git a/src/Entrypoint/Controller/Keyforge/Game/Detail/game_analyze.html.twig b/src/Entrypoint/Controller/Keyforge/Game/Detail/game_analyze.html.twig index 1607874b..508fea01 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Detail/game_analyze.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Game/Detail/game_analyze.html.twig @@ -11,6 +11,7 @@
+
diff --git a/src/Entrypoint/Controller/Keyforge/Shared/keyforge_base.html.twig b/src/Entrypoint/Controller/Keyforge/Shared/keyforge_base.html.twig index c1ee5144..6eba1e0f 100644 --- a/src/Entrypoint/Controller/Keyforge/Shared/keyforge_base.html.twig +++ b/src/Entrypoint/Controller/Keyforge/Shared/keyforge_base.html.twig @@ -127,7 +127,13 @@
  • {{ 'menu.settings'|trans }}
  • -
  • {{ 'menu.logout'|trans }}
  • + +
    + + +
    diff --git a/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php index 19c1a18e..d7c05441 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php @@ -13,6 +13,10 @@ public function __invoke(Request $request): Response { $this->assertIsLogged(); + if (false === $this->isCsrfTokenValid('keyforge_deck_tag_add', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new AssignTagToDeckCommand( $request->get('deckId'), $request->get('tagId', []), diff --git a/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php index 76720a54..3d5b03ff 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php @@ -16,6 +16,10 @@ public function __invoke(Request $request): Response { $this->assertIsLogged(); + if (false === $this->isCsrfTokenValid('keyforge_deck_tag_create', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new CreateTagCommand( Uuid::v4()->value(), $request->get('name'), diff --git a/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php index 5963f352..5929975c 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php @@ -14,6 +14,10 @@ public function __invoke(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_deck_tag_remove', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new RemoveTagCommand($request->get('id'))); return new Response('', Response::HTTP_OK); diff --git a/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php b/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php index fe8300ef..c9a54a84 100644 --- a/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php +++ b/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php @@ -18,6 +18,10 @@ public function __invoke(Request $request): Response throw new \Exception('Operation not supported'); } + if (false === $this->isCsrfTokenValid('admin_manage_accounts', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $this->bus->dispatch(new ApproveAccountCommand( $request->request->get('id'), $request->getMethod() === Request::METHOD_POST, diff --git a/src/Entrypoint/Controller/Shared/Admin/admin.html.twig b/src/Entrypoint/Controller/Shared/Admin/admin.html.twig index 791fc21a..c67192e5 100644 --- a/src/Entrypoint/Controller/Shared/Admin/admin.html.twig +++ b/src/Entrypoint/Controller/Shared/Admin/admin.html.twig @@ -48,6 +48,7 @@ url: '{{ path('admin_accounts') }}', data: { 'id': $(event.target).data('account'), + '_csrf_token': '{{ csrf_token('admin_manage_accounts') }}', }, }).done(function( ) { location.reload(); @@ -62,6 +63,7 @@ url: '{{ path('admin_accounts') }}', data: { 'id': $(event.target).data('account'), + '_csrf_token': '{{ csrf_token('admin_manage_accounts') }}', }, }).done(function( ) { location.reload(); diff --git a/src/Entrypoint/Controller/Shared/Login/login.html.twig b/src/Entrypoint/Controller/Shared/Login/login.html.twig index a24e8462..b5a6ce13 100644 --- a/src/Entrypoint/Controller/Shared/Login/login.html.twig +++ b/src/Entrypoint/Controller/Shared/Login/login.html.twig @@ -39,6 +39,8 @@
    + +
    diff --git a/src/Entrypoint/Controller/Shared/Login/register.html.twig b/src/Entrypoint/Controller/Shared/Login/register.html.twig index a5954553..fa4e0080 100644 --- a/src/Entrypoint/Controller/Shared/Login/register.html.twig +++ b/src/Entrypoint/Controller/Shared/Login/register.html.twig @@ -46,6 +46,8 @@
    + +
    diff --git a/src/Entrypoint/Controller/Shared/User/UserFriendsController.php b/src/Entrypoint/Controller/Shared/User/UserFriendsController.php index 2262e7a3..d26ae78f 100644 --- a/src/Entrypoint/Controller/Shared/User/UserFriendsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserFriendsController.php @@ -20,6 +20,12 @@ public function __invoke(Request $request): Response $user = $this->getUserWithRole(UserRole::ROLE_BASIC); $error = null; + if ($request->getMethod() !== Request::METHOD_GET) { + if (false === $this->isCsrfTokenValid('user_friends', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + } + if ($request->getMethod() === Request::METHOD_PATCH) { $error = $this->acceptFriend($request, $user); } diff --git a/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php b/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php index 60218826..c3c151a6 100644 --- a/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php @@ -199,6 +199,10 @@ public function acceptGame(Request $request): Response { $user = $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_game_accept', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $gameId = $request->get('game'); if (false === Uuid::isValid($gameId)) { @@ -250,6 +254,10 @@ public function rejectGame(Request $request): Response { $user = $this->getUserWithRole(UserRole::ROLE_KEYFORGE); + if (false === $this->isCsrfTokenValid('keyforge_game_reject', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + $gameId = $request->get('game'); if (false === Uuid::isValid($gameId)) { diff --git a/src/Entrypoint/Controller/Shared/User/UserSettingsController.php b/src/Entrypoint/Controller/Shared/User/UserSettingsController.php index 532dc00a..bcaafae9 100644 --- a/src/Entrypoint/Controller/Shared/User/UserSettingsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserSettingsController.php @@ -18,6 +18,10 @@ public function __invoke(Request $request): Response $error = null; if ($request->getMethod() === Request::METHOD_POST) { + if (false === $this->isCsrfTokenValid('user_settings_update', $request->get('_csrf_token'))) { + throw new \Exception('Invalid CSRF token'); + } + /** @var User $user */ $user = $this->security->getUser(); $newPassword = $request->request->get('settingsPassword'); diff --git a/src/Entrypoint/Controller/Shared/User/user_friends.html.twig b/src/Entrypoint/Controller/Shared/User/user_friends.html.twig index 0dae6c21..b17b0e50 100644 --- a/src/Entrypoint/Controller/Shared/User/user_friends.html.twig +++ b/src/Entrypoint/Controller/Shared/User/user_friends.html.twig @@ -83,6 +83,7 @@
    +
    @@ -96,6 +97,7 @@ url: '{{ path('user_friends') }}', data: { 'friendId': $(event.target).data('friend'), + '_csrf_token': '{{ csrf_token('user_friends') }}', }, }).done(function( ) { location.reload(); @@ -110,6 +112,7 @@ url: '{{ path('user_friends') }}', data: { 'friendId': $(event.target).data('friend'), + '_csrf_token': '{{ csrf_token('user_friends') }}', }, }).done(function( ) { location.reload(); diff --git a/src/Entrypoint/Controller/Shared/User/user_pending_games.html.twig b/src/Entrypoint/Controller/Shared/User/user_pending_games.html.twig index 557aceaf..0e0292f6 100644 --- a/src/Entrypoint/Controller/Shared/User/user_pending_games.html.twig +++ b/src/Entrypoint/Controller/Shared/User/user_pending_games.html.twig @@ -57,6 +57,7 @@ url: '{{ path('user_accept_game') }}', data: { 'game': $(event.target).data('game'), + '_csrf_token': '{{ csrf_token('keyforge_game_accept') }}', }, }).done(function( ) { location.reload(); @@ -71,6 +72,7 @@ url: '{{ path('user_reject_game') }}', data: { 'game': $(event.target).data('game'), + '_csrf_token': '{{ csrf_token('keyforge_game_reject') }}', }, }).done(function( ) { location.reload(); diff --git a/src/Entrypoint/Controller/Shared/User/user_settings.html.twig b/src/Entrypoint/Controller/Shared/User/user_settings.html.twig index a357ead9..cfebb376 100644 --- a/src/Entrypoint/Controller/Shared/User/user_settings.html.twig +++ b/src/Entrypoint/Controller/Shared/User/user_settings.html.twig @@ -58,6 +58,7 @@
    +
    From c45bfba78730c3d559a03b26f4d689d6347760a5 Mon Sep 17 00:00:00 2001 From: adnanmula Date: Wed, 19 Nov 2025 22:53:07 +0100 Subject: [PATCH 2/4] add headers on response --- config/packages/framework.yaml | 9 +++++---- .../Security/SecurityHeadersListener.php | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 src/Infrastructure/Security/SecurityHeadersListener.php diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 5de6ece4..d25d8a1a 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -2,13 +2,14 @@ framework: secret: '%env(APP_SECRET)%' csrf_protection: true http_method_override: false +# trusted_proxies: '' +# trusted_headers: [ "x-forwarded-for", "x-forwarded-proto", "x-forwarded-port", "x-forwarded-host" ] - # Enables session support. Note that the session will ONLY be started if you read or write from it. - # Remove or comment this section to explicitly disable session support. session: handler_id: null - cookie_secure: auto - cookie_samesite: lax + cookie_secure: true + cookie_httponly: true + cookie_samesite: 'strict' # error_controller: AdnanMula\Cards\Infrastructure\Security\ErrorHandler diff --git a/src/Infrastructure/Security/SecurityHeadersListener.php b/src/Infrastructure/Security/SecurityHeadersListener.php new file mode 100644 index 00000000..3c59c280 --- /dev/null +++ b/src/Infrastructure/Security/SecurityHeadersListener.php @@ -0,0 +1,20 @@ +getResponse(); + + $response->headers->set('X-Frame-Options', 'DENY'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('X-XSS-Protection', '1; mode=block'); + $response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade'); + } +} From 45439ff8b4f49df0b87546b7bf97113ca0aeddad Mon Sep 17 00:00:00 2001 From: adnanmula Date: Wed, 19 Nov 2025 23:55:53 +0100 Subject: [PATCH 3/4] add rate limiter --- .env.dist | 7 +- composer.json | 2 + composer.lock | 361 +++++++++++++----- config/packages/framework.yaml | 10 +- config/packages/lock.yaml | 2 + config/services.yaml | 3 + .../Security/RateLimitSubscriber.php | 75 ++++ .../Security/SecurityHeadersListener.php | 2 +- symfony.lock | 12 + 9 files changed, 371 insertions(+), 103 deletions(-) create mode 100644 config/packages/lock.yaml create mode 100644 src/Infrastructure/Security/RateLimitSubscriber.php diff --git a/.env.dist b/.env.dist index f3255a32..13e3e2ae 100644 --- a/.env.dist +++ b/.env.dist @@ -4,7 +4,7 @@ ###> symfony/framework-bundle ### APP_ENV=dev APP_SECRET=1afb856da5195dc873d635947d0a76cb -#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 +TRUSTED_PROXIES= #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### @@ -38,4 +38,7 @@ VIRTUAL_HOST= LETSENCRYPT_HOST= LETSENCRYPT_EMAIL= ADMINER_HOST= -KIBANA_HOST= \ No newline at end of file +KIBANA_HOST= + +#Rate limiter +LOCK_DSN=flock diff --git a/composer.json b/composer.json index cdb76e84..6c352fce 100644 --- a/composer.json +++ b/composer.json @@ -22,8 +22,10 @@ "symfony/flex": "^1.3.1", "symfony/framework-bundle": "7.2.*", "symfony/http-client": "7.2.*", + "symfony/lock": "7.2.*", "symfony/messenger": "7.2.*", "symfony/monolog-bundle": "^3.10", + "symfony/rate-limiter": "7.2.*", "symfony/security-bundle": "7.2.*", "symfony/translation": "7.2.*", "symfony/twig-bundle": "7.2.*", diff --git a/composer.lock b/composer.lock index 6f329236..0c290e9f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "60aee91fa77d1e92b7449d8af417a3b7", + "content-hash": "6c682d274b1c604bed6979a966b20b2d", "packages": [ { "name": "adnanmula/criteria", @@ -1108,35 +1108,36 @@ }, { "name": "nelmio/api-doc-bundle", - "version": "v5.6.5", + "version": "v5.8.1", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "eb0453607560e63bbbc6a746978933f77a787575" + "reference": "16e139b812320dc33c971dc21627068fcc1ed34c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/eb0453607560e63bbbc6a746978933f77a787575", - "reference": "eb0453607560e63bbbc6a746978933f77a787575", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/16e139b812320dc33c971dc21627068fcc1ed34c", + "reference": "16e139b812320dc33c971dc21627068fcc1ed34c", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "phpdocumentor/reflection-docblock": "^5.0", "phpdocumentor/type-resolver": "^1.8.2", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/config": "^6.4 || ^7.1", - "symfony/console": "^6.4 || ^7.1", - "symfony/dependency-injection": "^6.4 || ^7.1", + "symfony/config": "^6.4 || ^7.2", + "symfony/console": "^6.4 || ^7.2", + "symfony/dependency-injection": "^6.4 || ^7.2", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/framework-bundle": "^6.4 || ^7.1", - "symfony/http-foundation": "^6.4 || ^7.1", - "symfony/http-kernel": "^6.4 || ^7.1", - "symfony/options-resolver": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/routing": "^6.4 || ^7.1", + "symfony/framework-bundle": "^6.4 || ^7.2", + "symfony/http-foundation": "^6.4 || ^7.2", + "symfony/http-kernel": "^6.4 || ^7.2", + "symfony/options-resolver": "^6.4 || ^7.2", + "symfony/property-info": "^6.4 || ^7.2", + "symfony/routing": "^6.4 || ^7.2", + "symfony/type-info": "^7.2", "zircote/swagger-php": "^4.11.1 || ^5.0" }, "conflict": { @@ -1153,24 +1154,24 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpstan/phpstan-symfony": "^2.0", "phpunit/phpunit": "^10.5", - "symfony/asset": "^6.4 || ^7.1", - "symfony/browser-kit": "^6.4 || ^7.1", - "symfony/cache": "^6.4 || ^7.1", - "symfony/dom-crawler": "^6.4 || ^7.1", - "symfony/expression-language": "^6.4 || ^7.1", - "symfony/finder": "^6.4 || ^7.1", - "symfony/form": "^6.4 || ^7.1", - "symfony/phpunit-bridge": "^6.4 || ^7.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/security-csrf": "^6.4 || ^7.1", - "symfony/security-http": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "symfony/stopwatch": "^6.4 || ^7.1", - "symfony/templating": "^6.4 || ^7.1", - "symfony/translation": "^6.4 || ^7.1", - "symfony/twig-bundle": "^6.4 || ^7.1", - "symfony/uid": "^6.4 || ^7.1", - "symfony/validator": "^6.4 || ^7.1", + "symfony/asset": "^6.4 || ^7.2", + "symfony/browser-kit": "^6.4 || ^7.2", + "symfony/cache": "^6.4 || ^7.2", + "symfony/dom-crawler": "^6.4 || ^7.2", + "symfony/expression-language": "^6.4 || ^7.2", + "symfony/finder": "^6.4 || ^7.2", + "symfony/form": "^6.4 || ^7.2", + "symfony/phpunit-bridge": "^6.4 || ^7.2", + "symfony/property-access": "^6.4 || ^7.2", + "symfony/security-csrf": "^6.4 || ^7.2", + "symfony/security-http": "^6.4 || ^7.2", + "symfony/serializer": "^6.4 || ^7.2", + "symfony/stopwatch": "^6.4 || ^7.2", + "symfony/templating": "^6.4 || ^7.2", + "symfony/translation": "^6.4 || ^7.2", + "symfony/twig-bundle": "^6.4 || ^7.2", + "symfony/uid": "^6.4 || ^7.2", + "symfony/validator": "^6.4 || ^7.2", "willdurand/hateoas-bundle": "^2.7", "willdurand/negotiation": "^3.0" }, @@ -1219,7 +1220,7 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.6.5" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v5.8.1" }, "funding": [ { @@ -1227,7 +1228,7 @@ "type": "github" } ], - "time": "2025-10-20T08:34:48+00:00" + "time": "2025-11-14T20:06:10+00:00" }, { "name": "nikic/php-parser", @@ -1342,16 +1343,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90a04bcbf03784066f16038e87e23a0a83cee3c2", + "reference": "90a04bcbf03784066f16038e87e23a0a83cee3c2", "shasum": "" }, "require": { @@ -1400,22 +1401,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.4" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-17T21:13:10+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "431c02da15e566adb0ad9c5030fa6f6204d9de9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/431c02da15e566adb0ad9c5030fa6f6204d9de9e", + "reference": "431c02da15e566adb0ad9c5030fa6f6204d9de9e", "shasum": "" }, "require": { @@ -1458,9 +1459,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.1" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-18T07:51:16+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -3881,6 +3882,88 @@ ], "time": "2025-07-31T09:36:38+00:00" }, + { + "name": "symfony/lock", + "version": "v7.2.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/lock.git", + "reference": "538856cf96f064b0aa6e521bcfff863e46a98e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/lock/zipball/538856cf96f064b0aa6e521bcfff863e46a98e15", + "reference": "538856cf96f064b0aa6e521bcfff863e46a98e15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Lock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jérémy Derussé", + "email": "jeremy@derusse.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource", + "homepage": "https://symfony.com", + "keywords": [ + "cas", + "flock", + "locking", + "mutex", + "redlock", + "semaphore" + ], + "support": { + "source": "https://github.com/symfony/lock/tree/v7.2.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T17:03:27+00:00" + }, { "name": "symfony/messenger", "version": "v7.2.9", @@ -4775,6 +4858,80 @@ ], "time": "2025-07-10T08:29:33+00:00" }, + { + "name": "symfony/rate-limiter", + "version": "v7.2.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/rate-limiter.git", + "reference": "daae5da398aca84809aa6088371314a9cb88b42e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/daae5da398aca84809aa6088371314a9cb88b42e", + "reference": "daae5da398aca84809aa6088371314a9cb88b42e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/options-resolver": "^6.4|^7.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/lock": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\RateLimiter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter de Jong", + "email": "wouter@wouterj.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a Token Bucket implementation to rate limit input and output in your application", + "homepage": "https://symfony.com", + "keywords": [ + "limiter", + "rate-limiter" + ], + "support": { + "source": "https://github.com/symfony/rate-limiter/tree/v7.2.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:29:33+00:00" + }, { "name": "symfony/routing", "version": "v7.2.9", @@ -6240,16 +6397,16 @@ }, { "name": "zircote/swagger-php", - "version": "5.5.2", + "version": "5.7.3", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "0ca908380414596f5ed3a7ad33a04abb4cffe613" + "reference": "4d0d3086d7c876626167d198cec285e98d3629dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/0ca908380414596f5ed3a7ad33a04abb4cffe613", - "reference": "0ca908380414596f5ed3a7ad33a04abb4cffe613", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/4d0d3086d7c876626167d198cec285e98d3629dc", + "reference": "4d0d3086d7c876626167d198cec285e98d3629dc", "shasum": "" }, "require": { @@ -6260,7 +6417,7 @@ "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": "^5.0 || ^6.0 || ^7.0", - "symfony/yaml": "^5.0 || ^6.0 || ^7.0" + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "symfony/process": ">=6, <6.4.14" @@ -6322,9 +6479,9 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/5.5.2" + "source": "https://github.com/zircote/swagger-php/tree/5.7.3" }, - "time": "2025-10-27T04:40:08+00:00" + "time": "2025-11-17T20:56:13+00:00" } ], "packages-dev": [ @@ -7532,29 +7689,29 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.1.2", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", - "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { "composer-plugin-api": "^2.2", "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", @@ -7624,7 +7781,7 @@ "type": "thanks_dev" } ], - "time": "2025-07-17T20:45:56+00:00" + "time": "2025-11-11T04:32:07+00:00" }, { "name": "doctrine/collections", @@ -7972,33 +8129,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -8026,6 +8188,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -8038,9 +8201,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -8050,7 +8215,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -8058,26 +8223,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -8085,6 +8249,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -8109,7 +8274,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -8134,7 +8299,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -8142,7 +8307,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "myclabs/deep-copy", @@ -8522,11 +8687,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", + "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", "shasum": "" }, "require": { @@ -8571,7 +8736,7 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2025-11-11T15:18:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8909,16 +9074,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.4.2", + "version": "12.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea" + "reference": "d8f644d8d9bb904867f7a0aeb1bd306e0d966949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a94ea4d26d865875803b23aaf78c3c2c670ea2ea", - "reference": "a94ea4d26d865875803b23aaf78c3c2c670ea2ea", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d8f644d8d9bb904867f7a0aeb1bd306e0d966949", + "reference": "d8f644d8d9bb904867f7a0aeb1bd306e0d966949", "shasum": "" }, "require": { @@ -8986,7 +9151,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.2" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.3" }, "funding": [ { @@ -9010,7 +9175,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T08:41:39+00:00" + "time": "2025-11-13T07:20:26+00:00" }, { "name": "psr/http-factory", @@ -10220,16 +10385,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "06113cfdaf117fc2165f9cd040bd0f17fcd5242d" + "reference": "0525c73950de35ded110cffafb9892946d7771b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/06113cfdaf117fc2165f9cd040bd0f17fcd5242d", - "reference": "06113cfdaf117fc2165f9cd040bd0f17fcd5242d", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", "shasum": "" }, "require": { @@ -10295,7 +10460,7 @@ "type": "thanks_dev" } ], - "time": "2025-09-15T11:28:58+00:00" + "time": "2025-11-10T16:43:36+00:00" }, { "name": "staabm/side-effects-detector", @@ -10564,16 +10729,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -10602,7 +10767,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -10610,7 +10775,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index d25d8a1a..243d2d41 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -2,8 +2,8 @@ framework: secret: '%env(APP_SECRET)%' csrf_protection: true http_method_override: false -# trusted_proxies: '' -# trusted_headers: [ "x-forwarded-for", "x-forwarded-proto", "x-forwarded-port", "x-forwarded-host" ] + trusted_proxies: [ '%env(TRUSTED_PROXIES)%' ] + trusted_headers: [ "x-forwarded-for", "x-forwarded-proto", "x-forwarded-port", "x-forwarded-host" ] session: handler_id: null @@ -11,6 +11,12 @@ framework: cookie_httponly: true cookie_samesite: 'strict' + rate_limiter: + global_limit: + policy: sliding_window + limit: 50 + interval: '1 minute' + # error_controller: AdnanMula\Cards\Infrastructure\Security\ErrorHandler #esi: true diff --git a/config/packages/lock.yaml b/config/packages/lock.yaml new file mode 100644 index 00000000..574879f8 --- /dev/null +++ b/config/packages/lock.yaml @@ -0,0 +1,2 @@ +framework: + lock: '%env(LOCK_DSN)%' diff --git a/config/services.yaml b/config/services.yaml index 728a7983..99a1e4ad 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -56,6 +56,9 @@ services: AdnanMula\Cards\Infrastructure\Fixtures\FixturesRegistry: class: AdnanMula\Cards\Infrastructure\Fixtures\FixturesRegistry + Symfony\Component\RateLimiter\RateLimiterFactory: + alias: 'limiter.global_limit' + imports: - { resource: context/system/buses.yaml } - { resource: context/system/repositories.yaml } diff --git a/src/Infrastructure/Security/RateLimitSubscriber.php b/src/Infrastructure/Security/RateLimitSubscriber.php new file mode 100644 index 00000000..bb9b3fbf --- /dev/null +++ b/src/Infrastructure/Security/RateLimitSubscriber.php @@ -0,0 +1,75 @@ + ['onKernelRequest', 50], + 'kernel.response' => ['onKernelResponse', -50], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + + $ip = $request->getClientIp() ?? 'unknown'; + + $limiter = $this->globalLimiter->create($ip); + $limit = $limiter->consume(); + + $request->attributes->set('_rate_limit', $limit); + + if (false === $limit->isAccepted()) { + $response = new Response( + 'Too Many Requests', + 429, + [ + 'Retry-After' => $limit->getRetryAfter()->format('U'), + ], + ); + + $event->setResponse($response); + } + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (false === $event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + $response = $event->getResponse(); + + $limit = $request->attributes->get('_rate_limit'); + + if (null === $limit) { + return; + } + + $response->headers->set('X-RateLimit-Limit', (string) $limit->getLimit()); + $response->headers->set('X-RateLimit-Remaining', (string) $limit->getRemainingTokens()); + + if (false === $limit->isAccepted()) { + $response->headers->set('Retry-After', $limit->getRetryAfter()?->format('Y-m-d H:i:s')); + } + } +} diff --git a/src/Infrastructure/Security/SecurityHeadersListener.php b/src/Infrastructure/Security/SecurityHeadersListener.php index 3c59c280..f3b5118d 100644 --- a/src/Infrastructure/Security/SecurityHeadersListener.php +++ b/src/Infrastructure/Security/SecurityHeadersListener.php @@ -8,7 +8,7 @@ #[AsEventListener(event: 'kernel.response', method: 'onKernelResponse')] class SecurityHeadersListener { - public function onKernelResponse(ResponseEvent $event) + public function onKernelResponse(ResponseEvent $event): void { $response = $event->getResponse(); diff --git a/symfony.lock b/symfony.lock index eea0d59a..7f63a6cb 100644 --- a/symfony.lock +++ b/symfony.lock @@ -321,6 +321,18 @@ "symfony/http-kernel": { "version": "v5.1.0" }, + "symfony/lock": { + "version": "7.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.2", + "ref": "8e937ff2b4735d110af1770f242c1107fdab4c8e" + }, + "files": [ + "config/packages/lock.yaml" + ] + }, "symfony/messenger": { "version": "4.3", "recipe": { From aebd66c649af1d43ca7b99aa2c67dacc963e5e0e Mon Sep 17 00:00:00 2001 From: adnanmula Date: Fri, 21 Nov 2025 11:09:44 +0100 Subject: [PATCH 4/4] extract csrf validation --- .../Competition/CompetitionController.php | 24 ++++--------------- .../CreateCompetitionGameController.php | 4 +--- .../Alliance/GenerateAlliancesController.php | 4 +--- .../Deck/Import/ImportDeckController.php | 4 +--- .../UpdateNotes/UpdateDeckNotesController.php | 5 +--- .../UpdateDeckOwnershipController.php | 8 ++----- .../Game/Create/CreateGameController.php | 4 +--- .../Game/Detail/GameAnalyzeController.php | 4 +--- .../Keyforge/Tag/AssignTagController.php | 4 +--- .../Keyforge/Tag/CreateTagController.php | 5 +--- .../Keyforge/Tag/RemoveTagController.php | 5 +--- .../Shared/Admin/AdminAccountsController.php | 4 +--- .../Controller/Shared/Controller.php | 7 ++++++ .../Shared/User/UserFriendsController.php | 4 +--- .../User/UserNotificationsController.php | 10 ++------ .../Shared/User/UserSettingsController.php | 4 +--- 16 files changed, 28 insertions(+), 72 deletions(-) diff --git a/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php b/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php index 0f77d058..3b70903a 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php +++ b/src/Entrypoint/Controller/Keyforge/Competition/CompetitionController.php @@ -32,9 +32,7 @@ public function create(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { - if (false === $this->isCsrfTokenValid('keyforge_competition_create', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_create', $request->get('_csrf_token')); try { $this->bus->dispatch(new CreateCompetitionCommand( @@ -80,10 +78,7 @@ public function list(Request $request): Response public function start(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_competition_start', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_start', $request->get('_csrf_token')); $this->bus->dispatch(new StartCompetitionCommand( $request->get('competitionId'), @@ -96,10 +91,7 @@ public function start(Request $request): Response public function finish(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_competition_game_finish', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_game_finish', $request->get('_csrf_token')); $this->bus->dispatch(new FinishCompetitionCommand( $request->get('competitionId'), @@ -113,10 +105,7 @@ public function finish(Request $request): Response public function join(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_competition_join', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_join', $request->get('_csrf_token')); $this->bus->dispatch(new JoinCompetitionCommand( $request->get('id'), @@ -128,10 +117,7 @@ public function join(Request $request): Response public function leave(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_competition_leave', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_leave', $request->get('_csrf_token')); $this->bus->dispatch(new LeaveCompetitionCommand( $request->get('id'), diff --git a/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php b/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php index 0fb8d5e7..6aa0b749 100644 --- a/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php +++ b/src/Entrypoint/Controller/Keyforge/Competition/CreateCompetitionGameController.php @@ -14,9 +14,7 @@ public function __invoke(Request $request, string $fixtureId): Response { $this->assertIsLogged(); - if (false === $this->isCsrfTokenValid('keyforge_competition_game_create', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_competition_game_create', $request->get('_csrf_token')); $this->bus->dispatch(new CreateCompetitionGameCommand( $request->request->get('winner'), diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php index e93be1d9..3d84bc11 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/Alliance/GenerateAlliancesController.php @@ -23,9 +23,7 @@ public function __invoke(Request $request): Response if ($request->getMethod() === Request::METHOD_POST) { $payload = Json::decode($request->getContent()); - if (false === $this->isCsrfTokenValid('keyforge_alliance_generate', $payload['_csrf_token'])) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_alliance_generate', $request->get('_csrf_token')); try { $result = $this->extractResult( diff --git a/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php b/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php index 62aac7cf..0aa1f4f9 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/Import/ImportDeckController.php @@ -21,9 +21,7 @@ public function __invoke(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { - if (false === $this->isCsrfTokenValid('keyforge_deck_import', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_deck_import', $request->get('_csrf_token')); try { $deckId = $this->parseDeck($request->request->getString('deck')); diff --git a/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php b/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php index b90a8389..ff2211f2 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/UpdateNotes/UpdateDeckNotesController.php @@ -13,10 +13,7 @@ final class UpdateDeckNotesController extends Controller public function __invoke(Request $request): Response { $this->assertIsLogged(); - - if (false === $this->isCsrfTokenValid('keyforge_deck_update_notes', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_deck_update_notes', $request->get('_csrf_token')); /** @var User $user */ $user = $this->security->getUser(); diff --git a/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php b/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php index cb6ec381..2e6285bd 100644 --- a/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php +++ b/src/Entrypoint/Controller/Keyforge/Deck/UpdateOwnership/UpdateDeckOwnershipController.php @@ -39,17 +39,13 @@ public function __invoke(Request $request, string $id): Response } if ($request->getMethod() === Request::METHOD_POST) { - if (false === $this->isCsrfTokenValid('keyforge_ownership_update_add', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_ownership_update_add', $request->get('_csrf_token')); $this->deckRepository->addOwner(Uuid::from($id), $user->id()); } if ($request->getMethod() === Request::METHOD_DELETE) { - if (false === $this->isCsrfTokenValid('keyforge_ownership_update_remove', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_ownership_update_remove', $request->get('_csrf_token')); $this->deckRepository->removeOwner(Uuid::from($id), $user->id()); } diff --git a/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php b/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php index 52a7ae70..847f6a58 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php +++ b/src/Entrypoint/Controller/Keyforge/Game/Create/CreateGameController.php @@ -25,9 +25,7 @@ public function __invoke(Request $request): Response } if ($request->getMethod() === Request::METHOD_POST) { - if (false === $this->isCsrfTokenValid('keyforge_game_create', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_game_create', $request->get('_csrf_token')); try { $this->bus->dispatch(new CreateGameCommand( diff --git a/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php b/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php index 3da099fd..78bcef05 100644 --- a/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php +++ b/src/Entrypoint/Controller/Keyforge/Game/Detail/GameAnalyzeController.php @@ -41,9 +41,7 @@ public function __invoke(Request $request): Response } try { - if (false === $this->isCsrfTokenValid('keyforge_game_analyze', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_game_analyze', $request->get('_csrf_token')); $p = new GameLogParser(); $parsedLog = $p->execute($log); diff --git a/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php index d7c05441..8a446722 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/AssignTagController.php @@ -13,9 +13,7 @@ public function __invoke(Request $request): Response { $this->assertIsLogged(); - if (false === $this->isCsrfTokenValid('keyforge_deck_tag_add', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_deck_tag_add', $request->get('_csrf_token')); $this->bus->dispatch(new AssignTagToDeckCommand( $request->get('deckId'), diff --git a/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php index 3d5b03ff..754a47f6 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/CreateTagController.php @@ -15,10 +15,7 @@ final class CreateTagController extends Controller public function __invoke(Request $request): Response { $this->assertIsLogged(); - - if (false === $this->isCsrfTokenValid('keyforge_deck_tag_create', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_deck_tag_create', $request->get('_csrf_token')); $this->bus->dispatch(new CreateTagCommand( Uuid::v4()->value(), diff --git a/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php b/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php index 5929975c..7937ce85 100644 --- a/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php +++ b/src/Entrypoint/Controller/Keyforge/Tag/RemoveTagController.php @@ -13,10 +13,7 @@ final class RemoveTagController extends Controller public function __invoke(Request $request): Response { $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_deck_tag_remove', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_deck_tag_remove', $request->get('_csrf_token')); $this->bus->dispatch(new RemoveTagCommand($request->get('id'))); diff --git a/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php b/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php index c9a54a84..fae3d6ef 100644 --- a/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php +++ b/src/Entrypoint/Controller/Shared/Admin/AdminAccountsController.php @@ -18,9 +18,7 @@ public function __invoke(Request $request): Response throw new \Exception('Operation not supported'); } - if (false === $this->isCsrfTokenValid('admin_manage_accounts', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('admin_manage_accounts', $request->get('_csrf_token')); $this->bus->dispatch(new ApproveAccountCommand( $request->request->get('id'), diff --git a/src/Entrypoint/Controller/Shared/Controller.php b/src/Entrypoint/Controller/Shared/Controller.php index 20c3079c..adad3a64 100644 --- a/src/Entrypoint/Controller/Shared/Controller.php +++ b/src/Entrypoint/Controller/Shared/Controller.php @@ -80,4 +80,11 @@ final protected function setLocaleToUser(): void $this->localeSwitcher->setLocale($user->locale()->value); } + + final protected function validateCsrfToken(string $id, ?string $token): void + { + if (false === $this->isCsrfTokenValid($id, $token)) { + throw new \Exception('Invalid CSRF token'); + } + } } diff --git a/src/Entrypoint/Controller/Shared/User/UserFriendsController.php b/src/Entrypoint/Controller/Shared/User/UserFriendsController.php index d26ae78f..9a168b55 100644 --- a/src/Entrypoint/Controller/Shared/User/UserFriendsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserFriendsController.php @@ -21,9 +21,7 @@ public function __invoke(Request $request): Response $error = null; if ($request->getMethod() !== Request::METHOD_GET) { - if (false === $this->isCsrfTokenValid('user_friends', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('user_friends', $request->get('_csrf_token')); } if ($request->getMethod() === Request::METHOD_PATCH) { diff --git a/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php b/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php index c3c151a6..5c7842e2 100644 --- a/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserNotificationsController.php @@ -198,10 +198,7 @@ public function games(Request $request): Response public function acceptGame(Request $request): Response { $user = $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_game_accept', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_game_accept', $request->get('_csrf_token')); $gameId = $request->get('game'); @@ -253,10 +250,7 @@ public function acceptGame(Request $request): Response public function rejectGame(Request $request): Response { $user = $this->getUserWithRole(UserRole::ROLE_KEYFORGE); - - if (false === $this->isCsrfTokenValid('keyforge_game_reject', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('keyforge_game_reject', $request->get('_csrf_token')); $gameId = $request->get('game'); diff --git a/src/Entrypoint/Controller/Shared/User/UserSettingsController.php b/src/Entrypoint/Controller/Shared/User/UserSettingsController.php index bcaafae9..fe424957 100644 --- a/src/Entrypoint/Controller/Shared/User/UserSettingsController.php +++ b/src/Entrypoint/Controller/Shared/User/UserSettingsController.php @@ -18,9 +18,7 @@ public function __invoke(Request $request): Response $error = null; if ($request->getMethod() === Request::METHOD_POST) { - if (false === $this->isCsrfTokenValid('user_settings_update', $request->get('_csrf_token'))) { - throw new \Exception('Invalid CSRF token'); - } + $this->validateCsrfToken('user_settings_update', $request->get('_csrf_token')); /** @var User $user */ $user = $this->security->getUser();