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

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
glpi-11
csrf
api
jwt
oauth
auth
stateless
active high 2026-05-20 2026-05-20
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():

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