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",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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