From 246ff999d32e0d0890019811ba7c13e813c237c0 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Sun, 24 May 2026 14:15:12 +0200 Subject: [PATCH] Add CSRF token validation for quiz-related actions - Added CSRF validation to `enableQuiz`, `clearQuiz`, `deleteQuiz`, `toggleCandidate`, and `prepareElimination` actions. - Updated Twig templates to replace links with POST forms to include CSRF tokens. - Set HTTP method restrictions for related endpoints to `POST`. --- .../PrepareEliminationController.php | 7 ++++- src/Controller/Backoffice/QuizController.php | 29 ++++++++++++++---- .../quiz/tab_candidates_list.html.twig | 18 ++++++----- .../backoffice/quiz/tab_overview.html.twig | 30 +++++++++++++------ .../backoffice/quiz/tab_result.html.twig | 6 ++-- 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/Controller/Backoffice/PrepareEliminationController.php b/src/Controller/Backoffice/PrepareEliminationController.php index eda337b..f5a979c 100644 --- a/src/Controller/Backoffice/PrepareEliminationController.php +++ b/src/Controller/Backoffice/PrepareEliminationController.php @@ -24,9 +24,14 @@ final class PrepareEliminationController extends AbstractController '/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare', name: 'tvdt_prepare_elimination', requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID], + methods: ['POST'], )] - public function index(Season $season, Quiz $quiz): RedirectResponse + public function index(Season $season, Quiz $quiz, Request $request): RedirectResponse { + if (!$this->isCsrfTokenValid('prepare_elimination', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + $elimination = $this->eliminationFactory->createEliminationFromQuiz($quiz); return $this->redirectToRoute('tvdt_prepare_elimination_view', ['elimination' => $elimination->id]); diff --git a/src/Controller/Backoffice/QuizController.php b/src/Controller/Backoffice/QuizController.php index 293e836..bcc7db6 100644 --- a/src/Controller/Backoffice/QuizController.php +++ b/src/Controller/Backoffice/QuizController.php @@ -250,9 +250,14 @@ class QuizController extends AbstractController '/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable', name: 'tvdt_backoffice_enable', requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID.'|null'], + methods: ['POST'], )] - public function enableQuiz(Season $season, ?Quiz $quiz): RedirectResponse + public function enableQuiz(Season $season, ?Quiz $quiz, Request $request): RedirectResponse { + if (!$this->isCsrfTokenValid('enable_quiz', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + $season->activeQuiz = $quiz; $this->em->flush(); @@ -268,9 +273,14 @@ class QuizController extends AbstractController '/backoffice/quiz/{quiz}/clear', name: 'tvdt_backoffice_quiz_clear', requirements: ['quiz' => Requirement::UUID], + methods: ['POST'], )] - public function clearQuiz(Quiz $quiz): RedirectResponse + public function clearQuiz(Quiz $quiz, Request $request): RedirectResponse { + if (!$this->isCsrfTokenValid('clear_quiz', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + try { $this->quizRepository->clearQuiz($quiz); $this->addFlash('success', $this->translator->trans('Quiz cleared')); @@ -286,9 +296,14 @@ class QuizController extends AbstractController '/backoffice/quiz/{quiz}/delete', name: 'tvdt_backoffice_quiz_delete', requirements: ['quiz' => Requirement::UUID], + methods: ['POST'], )] - public function deleteQuiz(Quiz $quiz): RedirectResponse + public function deleteQuiz(Quiz $quiz, Request $request): RedirectResponse { + if (!$this->isCsrfTokenValid('delete_quiz', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + $this->quizRepository->deleteQuiz($quiz); $this->addFlash('success', $this->translator->trans('Quiz deleted')); @@ -347,10 +362,14 @@ class QuizController extends AbstractController '/backoffice/quiz/{quiz}/candidate/{candidate}/toggle', name: 'tvdt_backoffice_toggle_candidate', requirements: ['quiz' => Requirement::UUID, 'candidate' => Requirement::UUID], - methods: ['GET'], + methods: ['POST'], )] - public function toggleCandidate(Quiz $quiz, Candidate $candidate): RedirectResponse + public function toggleCandidate(Quiz $quiz, Candidate $candidate, Request $request): RedirectResponse { + if (!$this->isCsrfTokenValid('toggle_candidate', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + $quizCandidate = $this->quizCandidateRepository->findOneBy([ 'quiz' => $quiz, 'candidate' => $candidate, diff --git a/templates/backoffice/quiz/tab_candidates_list.html.twig b/templates/backoffice/quiz/tab_candidates_list.html.twig index 6c3ba98..19883ce 100644 --- a/templates/backoffice/quiz/tab_candidates_list.html.twig +++ b/templates/backoffice/quiz/tab_candidates_list.html.twig @@ -35,14 +35,16 @@ {% endif %} - - {% if quizCandidate == null or quizCandidate.active %} - {{ 'Deactivate'|trans }} - {% else %} - {{ 'Activate'|trans }} - {% endif %} - +
+ + +
{% endfor %} diff --git a/templates/backoffice/quiz/tab_overview.html.twig b/templates/backoffice/quiz/tab_overview.html.twig index ad945b8..3ab04c2 100644 --- a/templates/backoffice/quiz/tab_overview.html.twig +++ b/templates/backoffice/quiz/tab_overview.html.twig @@ -3,16 +3,24 @@
{% if quiz is same as (season.activeQuiz) %} - {{ 'Deactivate Quiz'|trans }} +
+ + +
{% else %} - {{ 'Make active'|trans }} +
+ + +
{% endif %} -
@@ -74,8 +82,10 @@ @@ -97,8 +107,10 @@ diff --git a/templates/backoffice/quiz/tab_result.html.twig b/templates/backoffice/quiz/tab_result.html.twig index d450c76..d1c9101 100644 --- a/templates/backoffice/quiz/tab_result.html.twig +++ b/templates/backoffice/quiz/tab_result.html.twig @@ -2,8 +2,10 @@