112 lines
5.3 KiB
Markdown
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`).
|