--- 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 `). ## 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`).