diff --git a/js/modules/GlpiInstall.js b/js/modules/GlpiInstall.js index 904e11a601f..648ea8f8592 100644 --- a/js/modules/GlpiInstall.js +++ b/js/modules/GlpiInstall.js @@ -30,6 +30,8 @@ * --------------------------------------------------------------------- */ +/* global getAjaxCsrfToken */ + import { ProgressBar } from './ProgressBar.js'; export async function init_database(progress_key) @@ -74,7 +76,15 @@ export async function init_database(progress_key) }, 1500); try { - await fetch(`${CFG_GLPI.root_doc}/install/init_database`, {method: 'POST'}); + await fetch(`${CFG_GLPI.root_doc}/install/init_database`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;', + 'X-Requested-With': 'XMLHttpRequest', + 'X-Glpi-Csrf-Token': getAjaxCsrfToken(), + }, + }); } catch { // DB installation is really long and can result in a `Proxy timeout` error. // It does not mean that the process is killed, it just mean that the proxy did not wait for the response diff --git a/js/modules/ObjectLock.js b/js/modules/ObjectLock.js index fe1da676aed..dec303cbb17 100644 --- a/js/modules/ObjectLock.js +++ b/js/modules/ObjectLock.js @@ -162,7 +162,8 @@ class ObjectLock { headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;', - 'X-Glpi-Csrf-Token': getAjaxCsrfToken() + 'X-Requested-With': 'XMLHttpRequest', + 'X-Glpi-Csrf-Token': getAjaxCsrfToken(), }, body: `unlock=1&id=${this.lock.id}` }).catch(() => { diff --git a/src/Glpi/Controller/ApiController.php b/src/Glpi/Controller/ApiController.php index c430f25cc1b..9f5153075dd 100644 --- a/src/Glpi/Controller/ApiController.php +++ b/src/Glpi/Controller/ApiController.php @@ -41,6 +41,7 @@ use Glpi\Http\HeaderlessStreamedResponse; use Glpi\Http\JSONResponse; use Glpi\Http\Request; +use Glpi\Security\Attribute\DisableCsrfChecks; use Glpi\Security\Attribute\SecurityStrategy; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -55,6 +56,7 @@ final class ApiController extends AbstractController 'request_parameters' => '.*', ] )] + #[DisableCsrfChecks()] #[SecurityStrategy(Firewall::STRATEGY_NO_CHECK)] public function __invoke(SymfonyRequest $request): SymfonyResponse { diff --git a/src/Glpi/Controller/ApiRestController.php b/src/Glpi/Controller/ApiRestController.php index 836dc3e4e65..a828673d029 100644 --- a/src/Glpi/Controller/ApiRestController.php +++ b/src/Glpi/Controller/ApiRestController.php @@ -38,6 +38,7 @@ use Glpi\Application\ErrorHandler; use Glpi\Http\Firewall; use Glpi\Http\HeaderlessStreamedResponse; +use Glpi\Security\Attribute\DisableCsrfChecks; use Glpi\Security\Attribute\SecurityStrategy; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -52,6 +53,7 @@ final class ApiRestController extends AbstractController 'request_parameters' => '.*', ] )] + #[DisableCsrfChecks()] #[SecurityStrategy(Firewall::STRATEGY_NO_CHECK)] public function __invoke(Request $request): Response { diff --git a/src/Glpi/Controller/CaldavController.php b/src/Glpi/Controller/CaldavController.php index 2db6d6f1807..77f57bbd6b1 100644 --- a/src/Glpi/Controller/CaldavController.php +++ b/src/Glpi/Controller/CaldavController.php @@ -36,6 +36,7 @@ use Glpi\Http\Firewall; use Glpi\Http\HeaderlessStreamedResponse; +use Glpi\Security\Attribute\DisableCsrfChecks; use Glpi\Security\Attribute\SecurityStrategy; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -50,6 +51,7 @@ final class CaldavController extends AbstractController 'request_parameters' => '.*', ] )] + #[DisableCsrfChecks()] #[SecurityStrategy(Firewall::STRATEGY_NO_CHECK)] public function __invoke(Request $request): Response { diff --git a/src/Glpi/Controller/IndexController.php b/src/Glpi/Controller/IndexController.php index 9651ad70558..2210c11eb3c 100644 --- a/src/Glpi/Controller/IndexController.php +++ b/src/Glpi/Controller/IndexController.php @@ -48,15 +48,10 @@ use Glpi\Security\Attribute\SecurityStrategy; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\Attribute\Route; final class IndexController extends AbstractController { - public function __construct(private HttpKernelInterface $http_kernel) - { - } - #[Route( [ "base" => "/", @@ -67,18 +62,6 @@ public function __construct(private HttpKernelInterface $http_kernel) #[SecurityStrategy(Firewall::STRATEGY_NO_CHECK)] public function __invoke(Request $request): Response { - if ( - $request->isMethod('POST') - && !$request->request->has('totp_code') - && $request->getContent() !== '' - ) { - // POST request from the inventory agent, forward it to the inventory controller. - $sub_request = $request->duplicate( - attributes: ['_controller' => InventoryController::class . '::index'] - ); - return $this->http_kernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); - } - return new HeaderlessStreamedResponse($this->call(...)); } diff --git a/src/Glpi/Controller/InventoryController.php b/src/Glpi/Controller/InventoryController.php index 1522cb347cc..16606d4485a 100644 --- a/src/Glpi/Controller/InventoryController.php +++ b/src/Glpi/Controller/InventoryController.php @@ -39,6 +39,7 @@ use Glpi\Http\Firewall; use Symfony\Component\HttpFoundation\Request; use Glpi\Inventory\Conf; +use Glpi\Security\Attribute\DisableCsrfChecks; use Glpi\Security\Attribute\SecurityStrategy; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; @@ -47,6 +48,8 @@ final class InventoryController extends AbstractController { public static bool $is_running = false; + + #[DisableCsrfChecks()] #[SecurityStrategy(Firewall::STRATEGY_NO_CHECK)] #[Route("/Inventory", name: "glpi_inventory", methods: ['GET', 'POST'])] #[Route("/front/inventory.php", name: "glpi_inventory_legacy", methods: ['GET', 'POST'])] diff --git a/src/Glpi/Http/Listener/CatchInventoryAgentRequestListener.php b/src/Glpi/Http/Listener/CatchInventoryAgentRequestListener.php new file mode 100644 index 00000000000..aca0837f143 --- /dev/null +++ b/src/Glpi/Http/Listener/CatchInventoryAgentRequestListener.php @@ -0,0 +1,68 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Http\Listener; + +use Glpi\Controller\InventoryController; +use Glpi\Kernel\ListenersPriority; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; + +final readonly class CatchInventoryAgentRequestListener implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::REQUEST => ['onKernelRequest', ListenersPriority::REQUEST_LISTENERS_PRIORITIES[self::class]], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + $request = $event->getRequest(); + + if ( + $request->getPathInfo() === '/' + && $request->isMethod('POST') + && !$request->request->has('totp_code') + && $request->getContent() !== '' + ) { + $request->attributes->set('_controller', InventoryController::class . '::index'); + } + } +} diff --git a/src/Glpi/Http/Listener/CheckCsrfListener.php b/src/Glpi/Http/Listener/CheckCsrfListener.php index 92b6b6fa5fe..fed2a241549 100644 --- a/src/Glpi/Http/Listener/CheckCsrfListener.php +++ b/src/Glpi/Http/Listener/CheckCsrfListener.php @@ -34,35 +34,47 @@ namespace Glpi\Http\Listener; +use Glpi\Security\Attribute\DisableCsrfChecks; use Session; -use Glpi\Kernel\ListenersPriority; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\KernelEvents; final readonly class CheckCsrfListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { - return [ - KernelEvents::REQUEST => ['onKernelRequest', ListenersPriority::REQUEST_LISTENERS_PRIORITIES[self::class]], - ]; + return [KernelEvents::CONTROLLER => 'onKernelController']; } - public function onKernelRequest(RequestEvent $event): void + public function onKernelController(ControllerEvent $event): void { if (!$event->isMainRequest()) { return; } + /** @var DisableCsrfChecks[] $attributes */ + $attributes = $event->getAttributes(DisableCsrfChecks::class); + if (\count($attributes) > 0) { + // CSRF checks are explicitely disabled for this controller. + return; + } + $request = $event->getRequest(); - // Security : check CSRF token - if (isAPI() || !$request->request->count()) { + $bodyless_methods = [ + Request::METHOD_GET, + Request::METHOD_HEAD, + Request::METHOD_OPTIONS, + Request::METHOD_TRACE, + ]; + if (in_array($request->getRealMethod(), $bodyless_methods)) { + // No CSRF checks if method is not supposed to have a body. return; } - if (preg_match('~(/(plugins|marketplace)/[^/]*|)/ajax/~', $request->getPathInfo()) === 1) { + if ($request->isXmlHttpRequest()) { // Keep CSRF token as many AJAX requests may be made at the same time. // This is due to the fact that read operations are often made using POST method (see #277). define('GLPI_KEEP_CSRF_TOKEN', true); diff --git a/src/Glpi/Kernel/ListenersPriority.php b/src/Glpi/Kernel/ListenersPriority.php index 91798f9e45d..a79748bef4f 100644 --- a/src/Glpi/Kernel/ListenersPriority.php +++ b/src/Glpi/Kernel/ListenersPriority.php @@ -71,7 +71,8 @@ final class ListenersPriority HttpListener\CheckMaintenanceListener::class => 430, - HttpListener\CheckCsrfListener::class => 420, + // This listener will forward to the inventory controller any inventory agent requests made on the index endpoint. + HttpListener\CatchInventoryAgentRequestListener::class => 420, // Executes the legacy controller scripts (`/ajax/*.php` or `/front/*.php` scripts) whenever the // requested URI matches an existing file. diff --git a/src/Glpi/Security/Attribute/DisableCsrfChecks.php b/src/Glpi/Security/Attribute/DisableCsrfChecks.php new file mode 100644 index 00000000000..bfeb4895f26 --- /dev/null +++ b/src/Glpi/Security/Attribute/DisableCsrfChecks.php @@ -0,0 +1,40 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\Security\Attribute; + +#[\Attribute(\Attribute::TARGET_METHOD)] +final readonly class DisableCsrfChecks +{ +} diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 449c3e064fd..7cbaef8179f 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -428,14 +428,21 @@ Cypress.Commands.add('enableDebugMode', () => { return; } - cy.request({ - method: 'POST', - url: '/ajax/switchdebug.php', - body: { - 'debug': 'on', - }, - }).then(() => { - cy.reload(); + cy.getCsrfToken().then((csrf) => { + cy.request({ + method: 'POST', + url: '/ajax/switchdebug.php', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'X-Glpi-Csrf-Token': csrf, + }, + body: { + 'debug': 'on', + }, + }).then(() => { + cy.reload(); + }); }); }); @@ -448,14 +455,22 @@ Cypress.Commands.add('disableDebugMode', () => { if (Cypress.$('#debug-toolbar-applet').length === 0) { return; } - cy.request({ - method: 'POST', - url: '/ajax/switchdebug.php', - body: { - 'debug': 'off', - }, - }).then(() => { - cy.reload(); + + cy.getCsrfToken().then((csrf) => { + cy.request({ + method: 'POST', + url: '/ajax/switchdebug.php', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + 'X-Glpi-Csrf-Token': csrf, + }, + body: { + 'debug': 'off', + }, + }).then(() => { + cy.reload(); + }); }); });