--- id: KB-PLUGIN-044 title: "GLPI 11 — Visibilidade nativa em listagens de plugin (tickets/projetos) e permissionamento espelhado: receita e armadilhas" domain: plugin-dev tags: - glpi11 - plugin - security - visibility - rights - profile - sqlprovider - ticket - project - hardening - mindscrum status: active severity: critical created_at: 2026-07-13 updated_at: 2026-07-13 applies_to: - GLPI 11.0.8 dev - plugin mindscrum 0.9.2 - qualquer plugin que liste tickets/projetos fora do Search nativo related_records: - KB-PLUGIN-028 - KB-PLUGIN-039 - KB-PLUGIN-043 --- # GLPI 11 — Visibilidade nativa em listagens de plugin e permissionamento espelhado ## O problema (achado em auditoria do mindscrum) Um plugin que consulta `glpi_tickets`/`glpi_projects` direto com `$DB->request()` e filtra só por `getEntitiesRestrictCriteria()` **vaza dados**: mostra a qualquer usuário com o right do plugin TODOS os itens da entidade — ignorando as regras nativas ("técnico vê só os seus/do grupo", "projeto visível a gerente/equipe"). O right do plugin NÃO substitui a visibilidade por item. ## Receita — tickets (a mesma do kanban do core) `CommonITILObject::getDataToDisplayOnKanban` faz (replicar): ```php use Glpi\Search\Provider\SQLProvider; $where = ['glpi_tickets.is_deleted' => 0] + getEntitiesRestrictCriteria('glpi_tickets'); $vis = SQLProvider::getDefaultWhereCriteria(\Ticket::class); if ($vis !== []) { $where[] = $vis; } $query = ['SELECT' => [/* campos PREFIXADOS glpi_tickets.x AS x */], 'FROM' => 'glpi_tickets', 'WHERE' => $where]; // ⚠️ o WHERE referencia JOINs com alias hasheado (glpi_tickets_users_...): // SEM os joins correspondentes → "Unknown column ..._users_.users_id" $linked = []; $join = SQLProvider::getDefaultJoinCriteria(\Ticket::class, 'glpi_tickets', $linked); if ($join !== []) { $query = array_merge_recursive($query, $join); } // joins de atores DUPLICAM linhas → deduplicar por id ($rows[$r['id']] = $r) ``` Armadilhas: (1) `getDefaultWhereCriteria` sem `getDefaultJoinCriteria` = SQL quebrado; (2) campos do SELECT precisam de prefixo de tabela (joins tornam `id` ambíguo); (3) dedup por id obrigatório. ## Receita — projetos ```php $vis = \Project::getVisibilityCriteria(); // ['LEFT JOIN'=>[], 'WHERE'=>[]] $vis['WHERE'] += getEntitiesRestrictCriteria('glpi_projects', '', '', 'auto'); // LEFT JOIN => (seus joins) + $vis['LEFT JOIN']; WHERE => $vis['WHERE'] + (seus filtros) // dedup por id (join com projectteams duplica) ``` ## Permissionamento espelhado (rights do plugin por perfil) Conceder o right do plugin "para todos os perfis central" (mitigação C do KB-PLUGIN-028) dá UPDATE até a Observer/Read-Only. Correto: espelhar o nativo — `UPDATE` do plugin somente para perfis cujo right `ticket` tem `UPDATE`: ```php $value = READ + (($ticket_rights & UPDATE) ? UPDATE : 0); addDefaultProfileInfos($profile_id, [RIGHT => $value], true); // drop_existing corrige base ``` Reinstalar via console aplica a correção (install idempotente). ## Guardas de item-alvo (defesa em profundidade) Right do plugin ≠ direito no item. TODA mutação deve carregar o alvo e delegar ao nativo: `canUpdateItem()` (criar/alterar tarefa, mover, campos), `canViewItem()` (comentar), `Ticket::canCreate()` + `canViewItem()` do projeto (spawn). Colocar as guardas NAS CLASSES de serviço (não só no endpoint) — valem para qualquer chamador e são testáveis em CLI. Teste E2E que prova: sessão com `ticket => READ` (sem READALL) deve ver só os chamados onde é ator; sessão com rights zerados deve ter toda mutação negada. ## Bônus — XSS de atributo em JS de plugin `div.textContent = s; return div.innerHTML` escapa `& < >` mas **não aspas** — inseguro para `title="..."`/`data-x="..."`. Usar replace de `[&<>"']`.