KB-PLUGIN-045: padrao de campo seguro p/ form anonimo (QuestionType+Applicator); KB-INFRA-005: enumeracao multi-linha + captcha ALTCHA

Modelo validado com QuestionTypeSafeCategory (categoria ITIL filtrada por
is_helpdeskvisible, applicator grava itilcategories_id). KB-INFRA-005 corrige a
enumeracao de rotas (multi-linha) e documenta o fix do /Altcha/Challenge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gemini 2026-07-15 21:57:43 -03:00
parent 5e5a467f07
commit 5c3cff2f81
3 changed files with 138 additions and 4 deletions

View file

@ -892,6 +892,25 @@
"severity": "critical",
"path": "records/plugin-dev/KB-PLUGIN-044-glpi11-native-visibility-in-plugin-boards.md",
"summary": "id: KB-PLUGIN-044"
},
{
"id": "KB-PLUGIN-045",
"title": "Padrao de campo seguro para formulario anonimo GLPI 11 (QuestionType + Applicator)",
"domain": "plugin-dev",
"tags": [
"glpi",
"glpi11",
"form-builder",
"question-type",
"form-destination",
"anonymous",
"security",
"extform"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-045-safe-anonymous-field-questiontype-applicator.md",
"summary": "id: KB-PLUGIN-045"
}
]
}

View file

@ -64,12 +64,18 @@ Por isso o form builder (controlador Symfony generico) quebrava, enquanto o rest
# Correcao aplicada
Adicionar as rotas de topo do GLPI 11 ao allowlist nos DOIS lugares.
Enumerar as rotas de topo:
Enumerar as rotas de topo — ATENCAO: muitas `#[Route(...)]` sao MULTI-LINHA (o path
vem na linha seguinte), entao um grep de linha unica PERDE rotas (ex.: `/Altcha/Challenge`).
Usar `-A2`:
```bash
grep -rhoE "#\[Route\(\s*[\"']/[A-Za-z0-9_]+" src/Glpi/Controller/ \
| sed -E "s/.*['\"]\/([A-Za-z0-9_]+).*/\1/" | sort -u
# -> Central Form GenericAjaxCrud Helpdesk Install Inventory front progress
grep -rhA2 "#\[Route(" src/Glpi/Controller/ \
| grep -oE '"/[A-Za-z0-9_]+' | sed 's|"/||' | sort -u
# 11.0.8 -> Altcha AuthLDAP Config Form GenericAjaxCrud Helpdesk Install Inventory
# Knowbase MFA Plugin ServiceCatalog Session UI api apirest caldav front progress status
```
Como `root_doc=""` faz TODAS essas baterem na raiz, adicionar o conjunto todo de uma vez
(exceto `Install`, por hardening) evita ficar caçando rota a rota. O GLPI aplica a propria
auth (Firewall) em cada rota — o allowlist do proxy e so roteamento, nao fronteira de seguranca.
1. `nginx.conf` — regex do `location`:
```
@ -94,6 +100,15 @@ curl -sk https://host/rota-inexistente | head -c 40 # -> "404 page not found"
```
No browser: recarregar (Ctrl+Shift+R) e salvar um formulario -> deve funcionar.
# Caso 2026-07-15 (2) — captcha ALTCHA em formulario anonimo falhava
Sintoma: "A verificacao falhou. Por favor, tente novamente mais tarde." no widget ALTCHA
("Nao sou um robo") + "Falha ao enviar o formulario" no submit. Causa: o widget busca o
desafio em `GET /Altcha/Challenge` (STRATEGY_NO_CHECK), rota que NAO estava no allowlist
(fora por causa do bug de enumeracao multi-linha acima). Fix: adicionadas ao allowlist as
rotas faltantes: `Altcha AuthLDAP Config Knowbase MFA ServiceCatalog Session UI apirest
caldav status`. Validar: `curl -sk https://host/Altcha/Challenge` deve dar 200 com JSON
`{"algorithm":"SHA-256","challenge":...}`.
# Regra para agentes
- 404 com corpo `404 page not found` (plain text) = proxy/Traefik nao roteou; 404 HTML = GLPI roteou mas nao achou a rota. Distinga SEMPRE pelo corpo antes de culpar a app ou um plugin.
- GLPI 11 em subcaminho depende de allowlist manual das rotas de topo (Symfony) em Traefik + nginx. A cada upgrade do GLPI, reenumerar as rotas de `src/Glpi/Controller` e ressincronizar as duas allowlists.

View file

@ -0,0 +1,100 @@
---
id: KB-PLUGIN-045
title: Padrao de campo seguro para formulario anonimo GLPI 11 (QuestionType + Applicator)
domain: plugin-dev
tags:
- glpi
- glpi11
- form-builder
- question-type
- form-destination
- anonymous
- security
- extform
status: active
severity: high
created_at: 2026-07-15
updated_at: 2026-07-15
applies_to:
- GLPI 11.0.x form builder nativo
- plugin extform (campos externos seguros)
- qualquer campo "bloqueado no anonimo" que precise ser exposto com seguranca
related_records:
- KB-PLUGIN-002
- KB-PLUGIN-005
- KB-PLUGIN-007
- KB-INFRA-005
---
# Contexto
O GLPI 11 esconde de usuarios NAO autenticados toda pergunta cujo tipo retorna
`isAllowedForUnauthenticatedAccess() === false` (Condition\Engine). Bloqueados: atores,
Item/ItemDropdown (inventario), UserDevice. Categoria ITIL e Localizacao entram via
ItemDropdown -> tambem bloqueados. Precisamos expor alguns desses campos em formularios
de acesso externo, de forma SEGURA (sem enumerar entidades internas).
# Padrao (modelo, validado com QuestionTypeSafeCategory)
Cada campo bloqueado-mas-necessario = TRES pecas.
## 1. QuestionType de plugin (estende AbstractQuestionType)
- `isAllowedForUnauthenticatedAccess(): bool => true`.
- `getCategory()` retorna a categoria do plugin (KB-PLUGIN-002, senao editor quebra no JS).
- `renderEndUserTemplate()`: monta o `<select>` **no servidor**, ja filtrado ao subconjunto
seguro. Para ITILCategory: `is_helpdeskvisible = 1` + escopo de entidade do form via
`(new DbUtils())->getEntitiesRestrictCriteria($table,'entities_id',$entities_id,true)`.
**NUNCA** expor endpoint AJAX de dropdown (seria enumeracao). A entidade do form vem de
`$question->getForm()->fields['entities_id']`.
- `prepareEndUserAnswer()`: REVALIDA no servidor — reconstroi o conjunto permitido e
descarta id fora dele (defesa contra POST adulterado). Retorna null se invalido.
- `formatRawAnswer()`: resolve id -> nome (ex.: `completename`) para exibicao.
## 2. Applicator silencioso (estende AbstractConfigField)
Necessario porque `AnswersSet::getAnswersByType()` usa match EXATO de classe (nao is_a),
entao a resposta do tipo de plugin e invisivel as estrategias nativas de Destination
(KB-PLUGIN-005). Implementar:
- `getLabel()`, `getWeight()` (> peso nativo; ITILCategoryField=40, usar 1000),
`getCategory(): Glpi\Form\Destination\CommonITILField\Category` (ex.: `Category::PROPERTIES`),
`getConfigClass()`, `getDefaultConfig()`, `renderConfigForm()` retornando '' (invisivel na UI).
- `applyConfiguratedValueToInputUsingAnswers($config,$input,$answers_set)`:
`$answers = $answers_set->getAnswersByType(MinhaQuestao::class);`
pega `(int) $answer->getRawAnswer()`, revalida (`ITILCategory::getById($id)`),
e seta `$input['itilcategories_id'] = $id;` (para Localizacao: `locations_id`).
- Config minima: classe implementando `JsonFieldInterface` (`jsonDeserialize` estatico + `jsonSerialize` = []).
## 3. Registro no plugin_init
```php
$m = QuestionTypesManager::getInstance();
$m->registerPluginCategory(new QuestionCategory());
$m->registerPluginQuestionType(new QuestionTypeSafeCategory());
FormDestinationManager::getInstance()->registerPluginCommonITILConfigField(
AbstractCommonITILFormDestination::class, // aplica a Ticket/Change/Problem
new SafeCategoryApplicator()
);
```
# Validacao padrao (executada)
1. Sonda no Kernel bootado: tipo registrado, `isAllowedForUnauthenticatedAccess()=true`,
metodos do editor sem excecao, applicator presente em `plugins_common_itil_config_fields`.
2. Render anonimo (curl com token, sem cookie): `<select>` mostra so o subconjunto seguro;
categoria nao-visivel NAO aparece.
3. Submit valido via AnswersHandler::saveAnswers -> ticket criado com o campo setado.
4. Submit adulterado (id fora do subconjunto) -> `prepareEndUserAnswer` rejeita ->
campo do ticket fica 0. Categoria proibida nunca e injetada.
# Armadilhas
- Metodos do editor DEVEM passar sem excecao, senao o GLPI aborta o plugin_init e o tipo
"some" (checar `glpi:plugin:list`; reativar). Ver KB-PLUGIN-002.
- `QuestionTypeItemDropdown` e `final` — nao estender (KB-PLUGIN-007).
- Ambiente atras de proxy em subcaminho: o form builder e o captcha so funcionam apos o
allowlist de rotas Symfony (KB-INFRA-005).
# Regra para agentes
Para expor no anonimo qualquer campo que o core bloqueia, NUNCA "desbloquear" o tipo nativo
(recria a vulnerabilidade de enumeracao). Criar um QuestionType de plugin com render
server-side ESCOPADO + revalidacao no submit, e um Applicator silencioso para mapear a
resposta ao campo do ticket. Filtrar sempre pelo flag de visibilidade do proprio GLPI
(`is_helpdeskvisible`) e/ou escopo de entidade.
# Classificacao
- Tipo: Padrao arquitetural reutilizavel (modelo para SafeCategory, SafeLocation, etc.).
- Reutilizacao: base de todos os campos seguros do plugin extform.