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>
103 lines
3.5 KiB
Markdown
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
|