diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 2c4b0d5df..f3c90c59e 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -13,7 +13,7 @@ permissions: jobs: coding-standards: name: "CS Fixer, PHPStan & TwigCS" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest steps: - name: "Checkout" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 72f48471d..5f3a49f70 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -16,7 +16,7 @@ env: jobs: phpunit: name: "PHP ${{ matrix.php }} using ${{ matrix.database }}" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest services: rabbitmq: image: rabbitmq:3-alpine @@ -93,7 +93,7 @@ jobs: phpunit_no_prefix: name: "PHP ${{ matrix.php }} using ${{ matrix.database }} without prefix" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest services: rabbitmq: image: rabbitmq:3-alpine diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 4ebd0516c..5268cb4d6 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -13,7 +13,7 @@ permissions: jobs: translations: name: "Translations" - runs-on: "ubuntu-20.04" + runs-on: ubuntu-latest strategy: matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0ed4c88..45b61efc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ * **[BC BREAK]** Convert 403 errors to 404 errors by @yguedidi in https://github.com/wallabag/wallabag/pull/8075 * `wallassets/` folder renamed to `build/` +## [2.6.11](https://github.com/wallabag/wallabag/tree/2.6.11) +[Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.10...2.6.11) + +### Security fix +* Protect actions with a CSRF token by @yguedidi in https://github.com/wallabag/wallabag/commit/99c8a06594d6ee7480ce4d041ccff3025b353656 + ## [2.6.10](https://github.com/wallabag/wallabag/tree/2.6.10) [Full Changelog](https://github.com/wallabag/wallabag/compare/2.6.9...2.6.10) diff --git a/app/config/config.yml b/app/config/config.yml index bbe36a365..19e139db1 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -29,6 +29,7 @@ framework: handler_id: session.handler.native_file save_path: "%kernel.project_dir%/var/sessions/%kernel.environment%" cookie_secure: auto + cookie_samesite: lax storage_factory_id: session.storage.factory.native fragments: ~ http_method_override: true diff --git a/app/config/wallabag.yml b/app/config/wallabag.yml index 2523aae65..29656236d 100644 --- a/app/config/wallabag.yml +++ b/app/config/wallabag.yml @@ -1,5 +1,5 @@ parameters: - wallabag.version: 2.6.10 + wallabag.version: 2.6.11 wallabag.paypal_url: "https://liberapay.com/wallabag/donate" wallabag.languages: en: 'English' diff --git a/assets/scss/_cards.scss b/assets/scss/_cards.scss index 6ae2b8e9b..f5f7348bf 100644 --- a/assets/scss/_cards.scss +++ b/assets/scss/_cards.scss @@ -179,6 +179,7 @@ a.original:not(.waves-effect) { .card-entry-tags a, .card-entry-labels a, .card-tag-labels a, +.card-tag-labels button, .card-entry-labels-hidden a, #list .chip a { text-decoration: none; diff --git a/assets/scss/_dark_theme.scss b/assets/scss/_dark_theme.scss index aca9049e0..1f8f71814 100644 --- a/assets/scss/_dark_theme.scss +++ b/assets/scss/_dark_theme.scss @@ -63,7 +63,9 @@ .input-field input:focus, .results-item, .sidenav li > a, - .sidenav li > a > i.material-icons { + .sidenav li > a > i.material-icons, + .sidenav li button, + .sidenav li button > i.material-icons { color: #dfdfdf; } @@ -88,6 +90,7 @@ .mass-action-tags .mass-action-tags-input.mass-action-tags-input, .sidenav li:not(.logo) > a:hover, + .sidenav li:not(.logo) button:hover, .sidenav .collapsible-header:hover, .sidenav.sidenav-fixed .collapsible-header:hover { background-color: #1d1d1d; diff --git a/assets/scss/_nav.scss b/assets/scss/_nav.scss index a1db43486..a61657c76 100644 --- a/assets/scss/_nav.scss +++ b/assets/scss/_nav.scss @@ -6,11 +6,32 @@ nav { line-height: initial; } +// adapted from anchor styles from node_modules/@materializecss/materialize/sass/components/_navbar.scss +nav ul button { + transition: background-color .3s; + font-size: 1rem; + color: #fff; + display: block; + padding: 0 15px; + cursor: pointer; + background: none; + border: 0; + + &:focus { + background: none; + } + + &:hover { + background-color: rgba(0 0 0 / 10%); + } +} + nav { input { color: #aaa; } + ul button:hover, ul a:hover { background-color: initial; } @@ -34,6 +55,7 @@ nav { justify-content: space-between; align-items: center; + button, a { padding: 10px 15px; } diff --git a/assets/scss/_sidenav.scss b/assets/scss/_sidenav.scss index d57992063..8f2be7dd3 100644 --- a/assets/scss/_sidenav.scss +++ b/assets/scss/_sidenav.scss @@ -12,6 +12,7 @@ background: initial; } + & button > i.material-icons.theme-toggle-icon, & > a > i.material-icons.theme-toggle-icon { float: none; margin-left: 0; @@ -22,6 +23,7 @@ margin: 0; } + &.sidenav-fixed button, &.sidenav-fixed a { font-size: 13px; line-height: 44px; @@ -41,7 +43,35 @@ } } -.bold > a { +// adapted from anchor styles from node_modules/@materializecss/materialize/sass/components/_sidenav.scss +.sidenav li button { + color: rgba(0 0 0 / 87%); + display: block; + font-size: 14px; + font-weight: 500; + height: 48px; + line-height: 48px; + padding: 0 (16px * 2); + width: 100%; + text-align: left; + + &:hover { + background-color: rgba(0 0 0 / 5%); + } + + & > i, + & > i.material-icons { + float: left; + height: 48px; + line-height: 48px; + margin: 0 (16px * 2) 0 0; + width: 24px; + color: rgba(0 0 0 / 54%); + } +} + +.bold > a, +.bold > button { font-weight: bold; } diff --git a/assets/scss/_various.scss b/assets/scss/_various.scss index ad0703afa..569c46155 100644 --- a/assets/scss/_various.scss +++ b/assets/scss/_various.scss @@ -1,3 +1,5 @@ +@use "variables"; + /* ========================================================================== * Various * ========================================================================== */ @@ -38,3 +40,18 @@ nav .input-field input { .tab { flex: 1; } + +.btn-link { + background: none; + border: 0; + padding: 0; + color: variables.$blue-accent-color; + + &:focus { + background: none; + } +} + +.inline-block { + display: inline-block; +} diff --git a/src/Controller/Api/DeveloperController.php b/src/Controller/Api/DeveloperController.php index 1fd1da554..2eb774f3e 100644 --- a/src/Controller/Api/DeveloperController.php +++ b/src/Controller/Api/DeveloperController.php @@ -6,6 +6,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; use Wallabag\Controller\AbstractController; @@ -73,7 +74,7 @@ class DeveloperController extends AbstractController public function deleteClientAction(Request $request, Client $client, EntityManagerInterface $entityManager, TranslatorInterface $translator) { if (!$this->isCsrfTokenValid('delete-client', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } if (null === $this->getUser() || $client->getUser()->getId() !== $this->getUser()->getId()) { diff --git a/src/Controller/ConfigController.php b/src/Controller/ConfigController.php index 017c84aa8..a5c7bdf1c 100644 --- a/src/Controller/ConfigController.php +++ b/src/Controller/ConfigController.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint; @@ -253,7 +254,7 @@ class ConfigController extends AbstractController public function disableOtpEmailAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -278,7 +279,7 @@ class ConfigController extends AbstractController public function otpEmailAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -306,7 +307,7 @@ class ConfigController extends AbstractController public function disableOtpAppAction(Request $request) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -333,7 +334,7 @@ class ConfigController extends AbstractController public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $user = $this->getUser(); @@ -392,7 +393,7 @@ class ConfigController extends AbstractController public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator) { if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $isValid = $googleAuthenticator->checkCode( @@ -425,20 +426,20 @@ class ConfigController extends AbstractController /** * @return RedirectResponse|JsonResponse */ - #[Route(path: '/generate-token', name: 'generate_token', methods: ['GET'])] + #[Route(path: '/generate-token', name: 'generate_token', methods: ['POST'])] #[IsGranted('EDIT_CONFIG')] public function generateTokenAction(Request $request) { + if (!$this->isCsrfTokenValid('generate-token', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $config = $this->getConfig(); $config->setFeedToken(Utils::generateToken()); $this->entityManager->persist($config); $this->entityManager->flush(); - if ($request->isXmlHttpRequest()) { - return new JsonResponse(['token' => $config->getFeedToken()]); - } - $this->addFlash( 'notice', 'flashes.config.notice.feed_token_updated' @@ -450,20 +451,20 @@ class ConfigController extends AbstractController /** * @return RedirectResponse|JsonResponse */ - #[Route(path: '/revoke-token', name: 'revoke_token', methods: ['GET'])] + #[Route(path: '/revoke-token', name: 'revoke_token', methods: ['POST'])] #[IsGranted('EDIT_CONFIG')] public function revokeTokenAction(Request $request) { + if (!$this->isCsrfTokenValid('revoke-token', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $config = $this->getConfig(); $config->setFeedToken(null); $this->entityManager->persist($config); $this->entityManager->flush(); - if ($request->isXmlHttpRequest()) { - return new JsonResponse(); - } - $this->addFlash( 'notice', 'flashes.config.notice.feed_token_revoked' @@ -477,10 +478,14 @@ class ConfigController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/tagging-rule/delete/{taggingRule}', name: 'delete_tagging_rule', methods: ['GET'], requirements: ['taggingRule' => '\d+'])] + #[Route(path: '/tagging-rule/delete/{taggingRule}', name: 'delete_tagging_rule', methods: ['POST'], requirements: ['taggingRule' => '\d+'])] #[IsGranted('DELETE', subject: 'taggingRule')] - public function deleteTaggingRuleAction(TaggingRule $taggingRule) + public function deleteTaggingRuleAction(Request $request, TaggingRule $taggingRule) { + if (!$this->isCsrfTokenValid('delete-tagging-rule', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $this->entityManager->remove($taggingRule); $this->entityManager->flush(); @@ -509,10 +514,14 @@ class ConfigController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/ignore-origin-user-rule/delete/{ignoreOriginUserRule}', name: 'delete_ignore_origin_rule', methods: ['GET'], requirements: ['ignoreOriginUserRule' => '\d+'])] + #[Route(path: '/ignore-origin-user-rule/delete/{ignoreOriginUserRule}', name: 'delete_ignore_origin_rule', methods: ['POST'], requirements: ['ignoreOriginUserRule' => '\d+'])] #[IsGranted('DELETE', subject: 'ignoreOriginUserRule')] - public function deleteIgnoreOriginRuleAction(IgnoreOriginUserRule $ignoreOriginUserRule) + public function deleteIgnoreOriginRuleAction(Request $request, IgnoreOriginUserRule $ignoreOriginUserRule) { + if (!$this->isCsrfTokenValid('delete-ignore-origin-rule', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $this->entityManager->remove($ignoreOriginUserRule); $this->entityManager->flush(); @@ -546,7 +555,7 @@ class ConfigController extends AbstractController public function resetAction(Request $request, string $type, AnnotationRepository $annotationRepository, EntryRepository $entryRepository, TaggingRuleRepository $taggingRuleRepository) { if (!$this->isCsrfTokenValid('reset-area', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } switch ($type) { @@ -602,7 +611,7 @@ class ConfigController extends AbstractController public function deleteAccountAction(Request $request, UserRepository $userRepository, TokenStorageInterface $tokenStorage) { if (!$this->isCsrfTokenValid('delete-account', $request->request->get('token'))) { - throw $this->createAccessDeniedException('Bad CSRF token.'); + throw new BadRequestHttpException('Bad CSRF token.'); } $enabledUsers = $userRepository->getSumEnabledUsers(); @@ -627,10 +636,14 @@ class ConfigController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/config/view-mode', name: 'switch_view_mode', methods: ['GET'])] + #[Route(path: '/config/view-mode', name: 'switch_view_mode', methods: ['POST'])] #[IsGranted('EDIT_CONFIG')] public function changeViewModeAction(Request $request) { + if (!$this->isCsrfTokenValid('switch-view-mode', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $user = $this->getUser(); $user->getConfig()->setListMode(!$user->getConfig()->getListMode()); @@ -649,10 +662,14 @@ class ConfigController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/locale/{language}', name: 'changeLocale', methods: ['GET'])] + #[Route(path: '/locale/{language}', name: 'changeLocale', methods: ['POST'])] #[IsGranted('PUBLIC_ACCESS')] public function setLocaleAction(Request $request, ValidatorInterface $validator, $language = null) { + if (!$this->isCsrfTokenValid('change-locale', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $errors = $validator->validate($language, new LocaleConstraint(['canonicalize' => true])); if (0 === \count($errors)) { diff --git a/src/Controller/EntryController.php b/src/Controller/EntryController.php index ebfebe40b..658bf57c5 100644 --- a/src/Controller/EntryController.php +++ b/src/Controller/EntryController.php @@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; @@ -52,6 +53,10 @@ class EntryController extends AbstractController #[IsGranted('EDIT_ENTRIES')] public function massAction(Request $request, TagRepository $tagRepository) { + if (!$this->isCsrfTokenValid('mass-action', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $values = $request->request->all(); $tagsToAdd = []; @@ -395,10 +400,14 @@ class EntryController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/reload/{id}', name: 'reload_entry', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/reload/{id}', name: 'reload_entry', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('RELOAD', subject: 'entry')] - public function reloadAction(Entry $entry) + public function reloadAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('reload-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $this->updateEntry($entry, 'entry_reloaded'); // if refreshing entry failed, don't save it @@ -422,10 +431,14 @@ class EntryController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/archive/{id}', name: 'archive_entry', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/archive/{id}', name: 'archive_entry', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('ARCHIVE', subject: 'entry')] public function toggleArchiveAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('archive-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->toggleArchive(); $this->entityManager->flush(); @@ -449,10 +462,14 @@ class EntryController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/star/{id}', name: 'star_entry', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/star/{id}', name: 'star_entry', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('STAR', subject: 'entry')] public function toggleStarAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('star-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->toggleStar(); $entry->updateStar($entry->isStarred()); $this->entityManager->flush(); @@ -477,10 +494,14 @@ class EntryController extends AbstractController * * @return RedirectResponse */ - #[Route(path: '/delete/{id}', name: 'delete_entry', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/delete/{id}', name: 'delete_entry', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('DELETE', subject: 'entry')] public function deleteEntryAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('delete-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + // generates the view url for this entry to check for redirection later // to avoid redirecting to the deleted entry. Ugh. $url = $this->generateUrl( @@ -513,10 +534,14 @@ class EntryController extends AbstractController * * @return Response */ - #[Route(path: '/share/{id}', name: 'share', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/share/{id}', name: 'share', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('SHARE', subject: 'entry')] - public function shareAction(Entry $entry) + public function shareAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('share-entry', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + if (null === $entry->getUid()) { $entry->generateUid(); @@ -534,10 +559,14 @@ class EntryController extends AbstractController * * @return Response */ - #[Route(path: '/share/delete/{id}', name: 'delete_share', methods: ['GET'], requirements: ['id' => '\d+'])] + #[Route(path: '/share/delete/{id}', name: 'delete_share', methods: ['POST'], requirements: ['id' => '\d+'])] #[IsGranted('UNSHARE', subject: 'entry')] - public function deleteShareAction(Entry $entry) + public function deleteShareAction(Request $request, Entry $entry) { + if (!$this->isCsrfTokenValid('delete-share', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->cleanUid(); $this->entityManager->persist($entry); diff --git a/src/Controller/TagController.php b/src/Controller/TagController.php index 38e9173ce..d921ddb4c 100644 --- a/src/Controller/TagController.php +++ b/src/Controller/TagController.php @@ -10,6 +10,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; @@ -85,10 +86,14 @@ class TagController extends AbstractController * * @return Response */ - #[Route(path: '/remove-tag/{entry}/{tag}', name: 'remove_tag', methods: ['GET'], requirements: ['entry' => '\d+', 'tag' => '\d+'])] + #[Route(path: '/remove-tag/{entry}/{tag}', name: 'remove_tag', methods: ['POST'], requirements: ['entry' => '\d+', 'tag' => '\d+'])] #[IsGranted('UNTAG', subject: 'entry')] public function removeTagFromEntry(Request $request, Entry $entry, Tag $tag) { + if (!$this->isCsrfTokenValid('remove-tag', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $entry->removeTag($tag); $this->entityManager->flush(); @@ -225,10 +230,14 @@ class TagController extends AbstractController * * @return Response */ - #[Route(path: '/tag/search/{filter}', name: 'tag_this_search', methods: ['GET'])] + #[Route(path: '/tag/search/{filter}', name: 'tag_this_search', methods: ['POST'])] #[IsGranted('CREATE_TAGS')] public function tagThisSearchAction($filter, Request $request, EntryRepository $entryRepository) { + if (!$this->isCsrfTokenValid('tag-this-search', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + $currentRoute = $request->query->has('currentRoute') ? $request->query->get('currentRoute') : ''; /** @var QueryBuilder $qb */ @@ -260,11 +269,15 @@ class TagController extends AbstractController * * @return Response */ - #[Route(path: '/tag/delete/{slug}', name: 'tag_delete', methods: ['GET'])] + #[Route(path: '/tag/delete/{slug}', name: 'tag_delete', methods: ['POST'])] #[ParamConverter('tag', options: ['mapping' => ['slug' => 'slug']])] #[IsGranted('DELETE', subject: 'tag')] public function removeTagAction(Tag $tag, Request $request, EntryRepository $entryRepository) { + if (!$this->isCsrfTokenValid('tag-delete', $request->request->get('token'))) { + throw new BadRequestHttpException('Bad CSRF token.'); + } + foreach ($tag->getEntriesByUserId($this->getUser()->getId()) as $entry) { $entryRepository->removeTag($this->getUser()->getId(), $tag); } diff --git a/templates/Config/index.html.twig b/templates/Config/index.html.twig index 51ec63776..8abeaf15a 100644 --- a/templates/Config/index.html.twig +++ b/templates/Config/index.html.twig @@ -182,48 +182,63 @@