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

3.5 KiB

id title domain tags status severity created_at updated_at applies_to
KB-PLUGIN-010 Padrao correto para chamadas AJAX autenticadas em plugins GLPI 11 plugin-dev
glpi
glpi11
ajax
csrf
security
symfony
fetch
plugin
active critical 2026-05-03 2026-05-03
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

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:

<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)

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
// 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