5.3 KiB
| id | title | domain | tags | status | severity | created_at | updated_at | applies_to | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| KB-PLUGIN-016 | Bypass de CSRF e Autenticação Nativa (Bearer Token) para APIs Customizadas no GLPI 11 | plugin-dev |
|
active | high | 2026-05-20 | 2026-05-20 |
|
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():
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.
// 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
scimmindfoi desenhado com o bypass baseado emGLPI_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).