knowledge-base/records/plugin-dev/KB-PLUGIN-010-ajax-csrf-x-glpi-csrf-token.md
Rodolpho Lopes 069cd141a8 KB-PLUGIN-010: ajax/ no GLPI 11 dispensa inc/includes.php
Confirmado no plugin sync: endpoint ajax sem include responde 302/403
(core bootado pelo LegacyFileLoadController). Corrige exemplo legado.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 20:46:52 +00:00

103 lines
3.5 KiB
Markdown

---
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
<meta property="glpi:csrf_token" content="TOKEN_AQUI" />
```
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
<?php
// GLPI 11: NÃO incluir inc/includes.php — o LegacyFileLoadController já bootou
// o core/autoload antes de invocar arquivos de ajax/ (igual a front/, ver
// KB-PLUGIN-028). O include legado pode quebrar dependendo do path do plugin.
Session::checkRight('config', UPDATE); // suficiente — kernel já validou CSRF
// lógica do endpoint...
```
> **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