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:
parent
5e5a467f07
commit
5c3cff2f81
3 changed files with 138 additions and 4 deletions
19
index.json
19
index.json
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Reference in a new issue