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", "severity": "critical",
"path": "records/plugin-dev/KB-PLUGIN-044-glpi11-native-visibility-in-plugin-boards.md", "path": "records/plugin-dev/KB-PLUGIN-044-glpi11-native-visibility-in-plugin-boards.md",
"summary": "id: KB-PLUGIN-044" "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 # Correcao aplicada
Adicionar as rotas de topo do GLPI 11 ao allowlist nos DOIS lugares. 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 ```bash
grep -rhoE "#\[Route\(\s*[\"']/[A-Za-z0-9_]+" src/Glpi/Controller/ \ grep -rhA2 "#\[Route(" src/Glpi/Controller/ \
| sed -E "s/.*['\"]\/([A-Za-z0-9_]+).*/\1/" | sort -u | grep -oE '"/[A-Za-z0-9_]+' | sed 's|"/||' | sort -u
# -> Central Form GenericAjaxCrud Helpdesk Install Inventory front progress # 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`: 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. 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 # 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. - 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. - 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.