--- id: KB-PLUGIN-010 title: Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11 domain: plugin-dev tags: - glpi - glpi11 - ajax - csrf - security - symfony - fetch - plugin status: active severity: critical created_at: 2026-05-03 updated_at: 2026-05-03 applies_to: - GLPI 11.0.x --- # Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11 ## Sintoma Chamadas `fetch` POST feitas por um plugin retornam HTTP 403. O plugin está corretamente instalado e o usuário está autenticado com o direito necessário. O erro ocorre mesmo que `Session::checkCSRF()` não seja chamado no PHP do plugin. ## Causa O GLPI 11 utiliza o kernel Symfony com um listener dedicado (`CheckCsrfListener`) que intercepta **todos os requests POST antes de o PHP do plugin executar**. Para requests AJAX (identificados pelo header `X-Requested-With: XMLHttpRequest`), ele exige o CSRF token no header **`X-Glpi-Csrf-Token`** — não no body da requisição. Fonte: `/var/www/glpi/src/Glpi/Kernel/Listener/ControllerListener/CheckCsrfListener.php` ```php if ($request->isXmlHttpRequest()) { Session::checkCSRF( ['_glpi_csrf_token' => $request->server->get('HTTP_X_GLPI_CSRF_TOKEN') ?? ''], preserve_token: true // token não é consumido — reutilizável em múltiplas chamadas ); } else { Session::checkCSRF($request->request->all()); } ``` ## Onde fica o token O token é renderizado pelo layout do GLPI em uma meta tag: ```html ``` Com `preserve_token: true`, o mesmo token permanece válido para todas as chamadas AJAX da sessão — não é consumido. ## Solucao ### JavaScript (plugin) ```js function csrfToken() { var meta = document.querySelector('meta[property="glpi:csrf_token"]'); return meta ? meta.getAttribute('content') : ''; } fetch('/plugins/meu-plugin/ajax/endpoint.php', { method: 'POST', credentials: 'same-origin', // envia o cookie de sessão headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', // identifica como AJAX para o kernel 'X-Glpi-Csrf-Token': csrfToken() // token exigido pelo CheckCsrfListener }, body: new URLSearchParams({ action: 'minha_action', key: 'valor' }) }); ``` ### PHP (endpoint do plugin) **Não chamar** `Session::checkCSRF()` — o kernel já fez a verificação. Apenas verificar o direito do usuário: ```php **Atualização (GLPI 11.0.x):** o `include('../../../inc/includes.php')` mostrado > em versões antigas deste padrão **não é mais necessário nem recomendado** em > `ajax/`. Confirmado no plugin `sync` (2026-06-16): `ajax/testconnection.php` sem > include responde 302 (GET sem sessão → `checkLoginUser` redirige) e 403 (POST > sem token CSRF), provando que o core é bootado pelo kernel sem o include. ## Comportamento esperado Com o padrão acima: - Kernel valida CSRF via header antes do PHP rodar - PHP recebe a requisição autenticada normalmente - Token não é consumido (`preserve_token: true`), então múltiplos POSTs consecutivos funcionam sem gerar novo token