From ec585b3ab2c87fcd7613c32df3e3fa331d928a0c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 28 Jan 2026 16:23:00 +0100 Subject: [PATCH 1/2] fix: Fix redirecting to the "Referer" URL --- ...tton_pending_release_message_controller.js | 4 + .../controllers/form-group_controller.js | 1 + .../form-ldap-connector_controller.js | 11 +- .../controllers/message-counter_controller.js | 8 +- app/src/Controller/DefaultController.php | 18 ++-- app/src/Controller/ImportController.php | 24 ++--- app/src/Controller/MessageController.php | 36 ++----- app/src/Controller/UserController.php | 23 ++-- app/src/Controller/WblistController.php | 18 ++-- .../PseudoReferrerSubscriber.php | 41 +++++++ app/src/Service/Referrer.php | 101 ++++++++++++++++++ 11 files changed, 206 insertions(+), 79 deletions(-) create mode 100644 app/src/EventSubscriber/PseudoReferrerSubscriber.php create mode 100644 app/src/Service/Referrer.php diff --git a/app/assets/controllers/button_pending_release_message_controller.js b/app/assets/controllers/button_pending_release_message_controller.js index 3e4cd955..1790d7bb 100644 --- a/app/assets/controllers/button_pending_release_message_controller.js +++ b/app/assets/controllers/button_pending_release_message_controller.js @@ -24,6 +24,10 @@ export default class extends Controller { const url = this.urlValue; const response = await fetch(url, { method: 'GET', + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, }); if (!response.ok) { diff --git a/app/assets/controllers/form-group_controller.js b/app/assets/controllers/form-group_controller.js index ccbf8c74..702a2c5a 100644 --- a/app/assets/controllers/form-group_controller.js +++ b/app/assets/controllers/form-group_controller.js @@ -30,6 +30,7 @@ btnSubmitTarget.disabled = true method: 'POST', headers: { 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', }, body: formData }) diff --git a/app/assets/controllers/form-ldap-connector_controller.js b/app/assets/controllers/form-ldap-connector_controller.js index c6dcc8ff..e9ebfae6 100644 --- a/app/assets/controllers/form-ldap-connector_controller.js +++ b/app/assets/controllers/form-ldap-connector_controller.js @@ -29,6 +29,7 @@ export default class extends Controller { method: 'POST', headers: { 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', }, body: formData }) @@ -74,6 +75,7 @@ export default class extends Controller { method: 'POST', headers: { 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', }, body: formData }) @@ -100,6 +102,7 @@ export default class extends Controller { method: 'POST', headers: { 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', }, body: formData }) @@ -113,8 +116,8 @@ export default class extends Controller { } } - - + + showHideGroupInfoEventClick(event) { this.showHideGroupInfo(event.target.checked); } @@ -140,7 +143,7 @@ export default class extends Controller { showHideBindInfoEventClick(event) { this.showHideBindInfo(event.target.checked); - } + } showHideBindInfo(checked) { const divBindingInfo = this.element.querySelector('#info-binding-ldap'); @@ -158,5 +161,5 @@ export default class extends Controller { }); } this.checkConnection(); - } + } } diff --git a/app/assets/controllers/message-counter_controller.js b/app/assets/controllers/message-counter_controller.js index 3e821e8e..baa6116b 100644 --- a/app/assets/controllers/message-counter_controller.js +++ b/app/assets/controllers/message-counter_controller.js @@ -25,7 +25,13 @@ export default class extends Controller { this[`${name}Target`].textContent = '...'; }); - fetch(this.urlValue) + fetch(this.urlValue, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); diff --git a/app/src/Controller/DefaultController.php b/app/src/Controller/DefaultController.php index be01d029..da1de2b8 100644 --- a/app/src/Controller/DefaultController.php +++ b/app/src/Controller/DefaultController.php @@ -7,6 +7,7 @@ use App\Entity\User; use App\Repository\UserRepository; use App\Repository\MsgrcptRepository; +use App\Service\Referrer; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; @@ -19,6 +20,7 @@ class DefaultController extends AbstractController { public function __construct( private Security $security, + private Referrer $referrer, ) { } @@ -49,11 +51,6 @@ public function setLocaleAction(Request $request, EntityManagerInterface $em, ?s $request->getSession()->set('_locale', $language); } - $url = $request->headers->get('referer'); - if (empty($url)) { - $url = $this->container->get('router')->generate('message'); - } - /** @var User $user */ $user = $this->getUser(); if (!$this->security->isGranted('ROLE_PREVIOUS_ADMIN')) { @@ -66,21 +63,22 @@ public function setLocaleAction(Request $request, EntityManagerInterface $em, ?s $em->persist($user); $em->flush(); - return new RedirectResponse($url); + return new RedirectResponse($this->referrer->get()); } #[Route('/per_page/{nbItems}', name: 'per_page')] public function perPage(int $nbItems, Request $request): Response { - $referer = $request->headers->get('referer'); $session = $request->getSession(); $session->set('perPage', $nbItems); - $urlParts = parse_url($referer); + + $referrer = $this->referrer->get(); + $urlParts = parse_url($referrer); parse_str($urlParts['query'] ?? '', $query); $query['page'] = 1; $newQuery = http_build_query($query); - $newReferer = strtok($referer, '?') . '?' . $newQuery; + $newReferrer = strtok($referrer, '?') . '?' . $newQuery; - return $referer ? $this->redirect($newReferer) : $this->redirectToRoute('homepage'); + return new RedirectResponse($newReferrer); } } diff --git a/app/src/Controller/ImportController.php b/app/src/Controller/ImportController.php index c7414c29..14ca87ed 100644 --- a/app/src/Controller/ImportController.php +++ b/app/src/Controller/ImportController.php @@ -10,6 +10,7 @@ use App\Entity\Wblist; use App\Form\ImportType; use App\Service\GroupService; +use App\Service\Referrer; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Filesystem\Filesystem; @@ -26,15 +27,12 @@ class ImportController extends AbstractController use ControllerCommonTrait; use ControllerWBListTrait; - private TranslatorInterface $translator; - private EntityManagerInterface $em; - private GroupService $groupService; - - public function __construct(TranslatorInterface $translator, EntityManagerInterface $em, GroupService $groupService) - { - $this->translator = $translator; - $this->em = $em; - $this->groupService = $groupService; + public function __construct( + private TranslatorInterface $translator, + private EntityManagerInterface $em, + private GroupService $groupService, + private Referrer $referrer, + ) { } #[Route(path: '/users', name: 'import_user_email', options: ['expose' => true])] @@ -66,8 +64,8 @@ public function index(Request $request): Response } else { $this->addFlash('danger', 'Generics.flash.BadFormatcsv'); } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + + return $this->redirect($this->referrer->get()); } return $this->render('import/index.html.twig', [ @@ -270,8 +268,8 @@ public function indexAlias(Request $request): Response } else { $this->addFlash('danger', 'Generics.flash.BadFormatcsv'); } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + + return $this->redirect($this->referrer->get()); } return $this->render('import/index_alias.html.twig', [ diff --git a/app/src/Controller/MessageController.php b/app/src/Controller/MessageController.php index bd5b0e30..592b2814 100644 --- a/app/src/Controller/MessageController.php +++ b/app/src/Controller/MessageController.php @@ -32,7 +32,8 @@ class MessageController extends AbstractController public function __construct( private TranslatorInterface $translator, private EntityManagerInterface $em, - private Service\MessageService $messageService + private Service\MessageService $messageService, + private Service\Referrer $referrer, ) { } @@ -249,11 +250,7 @@ public function deleteAction( $logService->addLog('delete', $mailId); } - if (!empty($request->headers->get('referer'))) { - return new RedirectResponse($request->headers->get('referer')); - } else { - return $this->redirectToRoute("message"); - } + return new RedirectResponse($this->referrer->get()); } #[Route(path: '/batch/{action}', name: 'message_batch', methods: 'POST', options: ['expose' => true])] @@ -290,8 +287,7 @@ public function batchMessageAction( } } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + return new RedirectResponse($this->referrer->get()); } #[Route(path: '/{partitionTag}/{mailId}/{rid}/authorized', name: 'message_authorized')] @@ -321,11 +317,7 @@ public function authorized( $logService->addLog('authorized', $mailId); } - if (!empty($request->headers->get('referer'))) { - return new RedirectResponse($request->headers->get('referer')); - } else { - return $this->redirectToRoute("message"); - } + return new RedirectResponse($this->referrer->get()); } #[Route(path: '/{partitionTag}/{mailId}/{rid}/banned', name: 'message_banned')] @@ -355,11 +347,7 @@ public function banned( $logService->addLog('banned', $mailId); } - if (!empty($request->headers->get('referer'))) { - return new RedirectResponse($request->headers->get('referer')); - } else { - return $this->redirectToRoute("message"); - } + return new RedirectResponse($this->referrer->get()); } /** @@ -391,11 +379,7 @@ public function restore( $this->addFlash('success', $this->translator->trans('Message.Flash.messagePendingRelease')); $logService->addLog('restore', $mailId); - if (!empty($request->headers->get('referer'))) { - return new RedirectResponse($request->headers->get('referer')); - } else { - return $this->redirectToRoute("message"); - } + return new RedirectResponse($this->referrer->get()); } #[IsGranted('ROLE_ADMIN')] @@ -412,8 +396,7 @@ public function authorizedDomain( $logService->addLog('authorize for domain ', $mailId); } - $referer = $request->headers->get('referer'); - return new RedirectResponse($referer); + return new RedirectResponse($this->referrer->get()); } #[IsGranted('ROLE_ADMIN')] @@ -430,8 +413,7 @@ public function bannedDomain( $logService->addLog('banned for domain ', $mailId); } - $referer = $request->headers->get('referer'); - return new RedirectResponse($referer); + return new RedirectResponse($this->referrer->get()); } /** diff --git a/app/src/Controller/UserController.php b/app/src/Controller/UserController.php index 0e29baed..7a8a20d3 100644 --- a/app/src/Controller/UserController.php +++ b/app/src/Controller/UserController.php @@ -10,6 +10,7 @@ use App\Form\UserType; use App\Repository\UserRepository; use App\Service\GroupService; +use App\Service\Referrer; use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -28,13 +29,11 @@ class UserController extends AbstractController { use ControllerWBListTrait; - private TranslatorInterface $translator; - private EntityManagerInterface $em; - - public function __construct(TranslatorInterface $translator, EntityManagerInterface $em) - { - $this->translator = $translator; - $this->em = $em; + public function __construct( + private TranslatorInterface $translator, + private EntityManagerInterface $em, + private Referrer $referrer, + ) { } #[Route(path: '/local', name: 'users_local_index', methods: 'GET')] @@ -302,8 +301,7 @@ public function deleteEmail(Request $request, User $user): Response $this->em->flush(); } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + return $this->redirect($this->referrer->get()); } #[Route(path: '/email/batchDelete', name: 'user_email_batch_delete', methods: 'POST')] @@ -314,8 +312,7 @@ public function batchDeleteEmail(Request $request): Response if (!$this->isCsrfTokenValid('delete user', $csrfToken)) { $this->addFlash('error', $this->translator->trans('Generics.flash.invalidCsrfToken', [], 'errors')); - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + return $this->redirect($this->referrer->get()); } foreach ($request->request->all('id') as $id) { @@ -325,8 +322,8 @@ public function batchDeleteEmail(Request $request): Response } } $this->em->flush(); - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + + return $this->redirect($this->referrer->get()); } #[Route(path: '/email/newUser', name: 'user_email_new', methods: 'GET|POST')] diff --git a/app/src/Controller/WblistController.php b/app/src/Controller/WblistController.php index d5461715..e0cbd54c 100644 --- a/app/src/Controller/WblistController.php +++ b/app/src/Controller/WblistController.php @@ -10,6 +10,7 @@ use App\Entity\Domain; use App\Repository\WblistRepository; use App\Service\LogService; +use App\Service\Referrer; use Doctrine\ORM\EntityManagerInterface; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -24,6 +25,7 @@ class WblistController extends AbstractController public function __construct( private TranslatorInterface $translator, private EntityManagerInterface $em, + private Referrer $referrer, ) { } @@ -106,11 +108,7 @@ public function deleteAction( $this->addFlash('error', 'Invalid csrf token'); } - if (!empty($request->headers->get('referer'))) { - return new RedirectResponse($request->headers->get('referer')); - } else { - return $this->redirectToRoute("message"); - } + return new RedirectResponse($this->referrer->get()); } private function deleteWbList( @@ -153,8 +151,7 @@ public function batchDeleteAction(Request $request): RedirectResponse $this->em->getRepository(Wblist::class)->delete($mailInfo[0], $mailInfo[1], $mailInfo[2]); } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + return new RedirectResponse($this->referrer->get()); } @@ -185,8 +182,7 @@ public function batchWbListAction(Request $request, ?string $action = null): Red } } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + return new RedirectResponse($this->referrer->get()); } #[Route(path: '/wblist/admin/import', name: 'import_wblist', options: ['expose' => true])] @@ -206,8 +202,8 @@ public function importWbListAction(Request $request): Response } else { $this->addFlash('danger', 'Generics.flash.BadFormatcsv'); } - $referer = $request->headers->get('referer'); - return $this->redirect($referer); + + return new RedirectResponse($this->referrer->get()); } return $this->render('import/index_wblist.html.twig', [ 'controller_name' => 'ImportController', diff --git a/app/src/EventSubscriber/PseudoReferrerSubscriber.php b/app/src/EventSubscriber/PseudoReferrerSubscriber.php new file mode 100644 index 00000000..a46d2081 --- /dev/null +++ b/app/src/EventSubscriber/PseudoReferrerSubscriber.php @@ -0,0 +1,41 @@ +isMainRequest()) { + // Ignore sub-requests such as embedded controllers or internal + // redirects. + return; + } + + $this->referrer->savePseudoReferrer(); + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::FINISH_REQUEST => 'savePseudoReferrer', + ]; + } +} diff --git a/app/src/Service/Referrer.php b/app/src/Service/Referrer.php new file mode 100644 index 00000000..4246d111 --- /dev/null +++ b/app/src/Service/Referrer.php @@ -0,0 +1,101 @@ +requestStack->getCurrentRequest(); + $session = $request->getSession(); + + $lastGetUrl = $session->get('_referrer', ''); + $referrer = $request->headers->get('Referer', $lastGetUrl); + + if ($this->isSafeUrl($referrer)) { + return $referrer; + } else { + return $this->urlGenerator->generate('homepage'); + } + } + + /** + * Save the current URI in session to be used later as referrer if the + * Referer header is missing. + * + * The URI is saved only if: + * - it's a GET request + * - it has been initiated by the user navigating in the app + * + * The method does its best to exclude requests made with JavaScript. + * In particular, requests performed with the `fetch()` method must send + * the `X-Requested-With: XMLHttpRequest` header to be ignored by this + * method. + */ + public function savePseudoReferrer(): void + { + $request = $this->requestStack->getCurrentRequest(); + $session = $request->getSession(); + + $fetchMode = $request->headers->get('Sec-Fetch-Mode', 'navigate'); + // If "navigate", the request is initiated by navigation between HTML + // documents. In our case, it can also be "cors" as Turbo loads pages + // with fetch requests. + $isMainNavigation = $fetchMode === 'navigate' || $fetchMode === 'cors'; + + $isTurboFrameRequest = $request->headers->get('turbo-frame') !== null; + $isJSRequest = $isTurboFrameRequest || $request->isXmlHttpRequest(); + + if ($request->isMethod('GET') && $isMainNavigation && !$isJSRequest) { + $session->set('_referrer', $request->getUri()); + } + } + + /** + * Return whether the given URL targets the current server. + * + * This is used to avoid attacks redirecting users to an external URL. + * + * Note that it doesn't check if the path is a valid route. + */ + private function isSafeUrl(string $url): bool + { + $isHttpUrl = str_starts_with($url, 'http://') || str_starts_with($url, 'https://'); + $isAbsolutePath = str_starts_with($url, '/') && !str_starts_with($url, '//'); + + if ($isHttpUrl) { + // If the URL is an HTTP(S) URL, make sure that it targets the application URL. + $appUrl = rtrim($this->appUrl, '/') . '/'; + return str_starts_with($url, $appUrl); + } elseif ($isAbsolutePath) { + return true; + } else { + return false; + } + } +} From 6920a9701054cae88a757c809ad5340a44585e3e Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 29 Jan 2026 14:11:24 +0100 Subject: [PATCH 2/2] release: Publish version 2.4.4 --- .env.example | 2 +- CHANGELOG.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ec6550e9..90ba5752 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Which images to pull for production deployment -VERSION=2.4.3 +VERSION=2.4.4 # Impact containers names, see # https://docs.docker.com/compose/how-tos/environment-variables/envvars/#compose_project_name diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cffdb7f..24a544d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog of AgentJ +## 2026-01-29 - 2.4.4 + +### Bug fixes + +- Fix redirecting to the "Referer" URL ([ec585b3a](https://github.com/Probesys/agentj/commit/ec585b3a)) + ## 2026-01-16 - 2.4.3 ### Bug fixes