knowledge-base/records/plugin-dev/KB-PLUGIN-016-glpi11-csrf-bypass-and-api-auth.md

112 lines
5.3 KiB
Markdown

---
id: KB-PLUGIN-016
title: "Bypass de CSRF e Autenticação Nativa (Bearer Token) para APIs Customizadas no GLPI 11"
domain: plugin-dev
tags:
- glpi-11
- csrf
- api
- jwt
- oauth
- auth
- stateless
status: active
severity: high
created_at: 2026-05-20
updated_at: 2026-05-20
applies_to:
- GLPI 11+
- Plugins com APIs customizadas
---
# Bypass de CSRF e Autenticação Nativa para APIs Customizadas no GLPI 11
## Contexto
No GLPI 11, a segurança do roteador principal e do Kernel do Symfony foi reforçada. A constante `GLPI_USE_CSRF_CHECK` passou a ser ignorada por motivos de segurança, e todas as rotas (incluindo as de compatibilidade "LegacyRoutes" mapeadas via `public/index.php`) sofrem interceptação do `CheckCsrfListener` se não forem explicitamente isentas.
Isso bloqueia requisições `POST` (como payloads JSON-RPC) feitas para endpoints customizados criados dentro de plugins (ex.: `ajax/mcp.php` ou `public/index.php`), retornando erro de "Ação não permitida" ou redirecionamento de sessão, impossibilitando a criação de APIs independentes dentro dos plugins usando a infraestrutura convencional sem modificações.
## Causa Raiz
O `CheckCsrfListener` no Kernel do GLPI 11 bloqueia requisições `POST` sem token CSRF válido a menos que o endpoint seja registrado como um **recurso stateless** (sem estado). O GLPI Web Frontend requer que as requisições possuam estado (Session Cookies), bloqueando chamadas puras de API (ex: Server-to-Server via MCP) que dependem exclusivamente de Cabeçalhos de Autorização (`Authorization: Bearer <TOKEN>`).
## Resolução e Arquitetura Recomendada
A solução exige dois passos: isentar o endpoint customizado do controle de estado (e consequentemente do CSRF) e implementar a validação nativa do JWT dentro do escopo do endpoint, injetando o usuário autenticado na sessão corrente.
### 1. Registrar a Rota como Stateless
No arquivo `setup.php` do seu plugin, dentro da função `plugin_init_seuplugin()`, registre o script PHP (ex: `ajax/mcp.php`) como uma rota stateless através do `SessionManager`. Essa regra será lida pelo `SessionManager::isResourceStateless()`:
```php
function plugin_init_mcprotocol() {
global $PLUGIN_HOOKS;
$PLUGIN_HOOKS['csrf_compliant']['mcprotocol'] = true;
// Registra o endpoint da API como stateless para ignorar o CSRF e Session Checks no GLPI 11
if (class_exists('\Glpi\Http\SessionManager') && method_exists('\Glpi\Http\SessionManager', 'registerPluginStatelessPath')) {
\Glpi\Http\SessionManager::registerPluginStatelessPath('mcprotocol', '#ajax/mcp\.php#');
}
}
```
### 2. Validar JWT e Injetar na Sessão (Herança de Permissões)
No seu arquivo de endpoint da API (`ajax/mcp.php`), carregue as dependências nativas (incluindo `inc/includes.php`). Valide o Bearer token JWT diretamente usando a chave pública nativa do GLPI (localizada em `GLPI_CONFIG_DIR . '/oauth.pub'`) e depois estabeleça as variáveis globais de sessão para garantir que todas as chamadas subjacentes das classes GLPI (ex: `Ticket`, `Project`) atuem em nome do usuário correto.
```php
// ajax/mcp.php
$AJAX_INCLUDE = 1;
define('GLPI_ROOT', '../../..');
// Hack de bypass: força o método como GET antes do includes.php carregar
// Para evitar possíveis verificações antecipadas em versões transicionais
$realMethod = $_SERVER['REQUEST_METHOD'];
$_SERVER['REQUEST_METHOD'] = 'GET';
include (GLPI_ROOT . "/inc/includes.php");
$_SERVER['REQUEST_METHOD'] = $realMethod;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
header('Content-Type: application/json');
// 1. Extração do Token
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
if (empty($authHeader) || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
echo json_encode(["error" => "Unauthorized - Bearer token missing"]);
exit;
}
$jwt = $matches[1];
// ATENÇÃO: GLPI 11 exporta a chave pública no diretório principal de configs
$publicKeyPath = GLPI_CONFIG_DIR . '/oauth.pub';
try {
// 2. Validação Nativa
$publicKey = file_get_contents($publicKeyPath);
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
$userId = $decoded->sub ?? null;
if (!$userId) {
throw new Exception("Token missing 'sub' claim");
}
// 3. Injeção de Sessão (Herança de LGPD / Permissões)
$_SESSION['glpiID'] = $userId;
$_SESSION['glpiname'] = "API User";
// É recomendado carregar outras flags se for interagir com entidades complexas
} catch (Exception $e) {
echo json_encode(["error" => "Invalid token"]);
exit;
}
// 4. Fluxo normal da API a partir daqui
$payload = json_decode(file_get_contents('php://input'), true);
echo json_encode(["status" => "success", "user_id" => $userId]);
```
## Aprendizados Chave
- O `scimmind` foi desenhado com o bypass baseado em `GLPI_USE_CSRF_CHECK`, o qual está obsoleto no GLPI 11.
- A diretriz recomendada pela Teclib (GLPI 11) para pontos de acesso isolados é o uso do `SessionManager::registerPluginStatelessPath`.
- Não adianta validar a requisição com sucesso e não definir o `$_SESSION['glpiID']`. O ecosistema do GLPI é intrinsicamente atrelado a `$_SESSION`, e falhar em instanciá-lo antes de acionar métodos da classe (como buscar itens de DB) levará a falhas de acesso e violação das entidades (`Entity`).