# Relatório 08 - Mitigações V2-V6 e varredura complementar (2026-07-02)

**Escopo:** implementação das mitigações pendentes do relatório de incidentes recorrentes do Boletim Oficial (auditoria IndicadoresTI/ChamadosTI) e correção dos achados de uma nova varredura completa do projeto.
**Referências:** Relatórios 01-07, `.cursor/rules/rules.mdc` (seção REGRAS DE SEGURANÇA).

## 1. V2 (Média) - SQL cru em `getChamadosAbertos`

- **Local:** `app/Http/Services/IndicadoresTi/IndicadoresTiService.php`
- **Vetor:** `orderByRaw('FIELD(id, ' . implode(',', $abas) . ')')` interpolava valores de sessão em SQL cru (violação da regra #1). O risco real era baixo (valores inteiros validados por `exists()` em `abrirAba`), mas a construção era insegura por padrão.
- **Correção:**
  - Normalização com `array_map('intval', ...)` na leitura da sessão (defesa em profundidade).
  - Ordenação com bindings: `orderByRaw('FIELD(id' . str_repeat(', ?', count($abas)) . ')', $abas)`.
- **Verificação:** módulo de chamados abre as abas na mesma ordem de antes; nenhum valor de sessão chega ao SQL sem binding.

## 2. V4 (Baixa/Média) - Autocompletes sem throttle e com CPF completo

- **Local:** `routes/admin.php`, `app/Providers/RouteServiceProvider.php`, `IndicadoresTiService::buscarSolicitantes()`
- **Vetor:** as rotas `admin/indicadoresTi/clientes/buscar` e `admin/indicadoresTi/unidades/buscar` (GET, autenticadas) não tinham rate limiting e retornavam CPF completo de clientes, permitindo scraping por usuário interno.
- **Correção:**
  - Novo rate limiter nomeado `autocomplete` (30 req/min por usuário autenticado ou IP) registrado no `RouteServiceProvider`.
  - `->middleware('throttle:autocomplete')` aplicado nas duas rotas.
  - CPF mascarado no payload JSON do autocomplete de clientes: `***.***.***-XX` (2 últimos dígitos), via helper `mascararCpf()`. O `id` do cliente segue íntegro para a seleção; a checagem de duplicidade por CPF permanece server-side em `storeCliente`.
- **Comportamento preservado:** o front-end (`public/pmar/js/indicadoresTi.js`) usa o campo `cpf` apenas para exibição no dropdown; nada quebra com o mascaramento.

## 3. V5 (Baixa) - Validação inline em `storeCliente`

- **Local:** `app/Http/Controllers/IndicadoresTi/IndicadoresTiController.php`
- **Vetor:** validação inline via `$request->validate()` (violação da regra #17).
- **Correção:** criada `app/Http/Requests/IndicadoresTiClienteRequest.php` com regras, mensagens traduzíveis (`__()`) e a exigência de CPF ou matrícula movida para `withValidator()`. O controller passou a receber a FormRequest e usar `validated()`.
- **Comportamento preservado:** o endpoint é JSON (o front envia `X-Requested-With: XMLHttpRequest`), então a `ValidationException` continua respondendo `422` com campo `message`, que é o que o JS consome. As respostas `409` de duplicidade permanecem no controller.

## 4. V6 (Baixa/Performance) - Consultas em loop por setor/unidade

- **Local:** `IndicadoresTiService::getOutrosChamadosPorSetor()`, `getTodosChamadosPorSetor()`, `getOutrosChamadosPorUnidade()`
- **Vetor:** uma query paginada por setor/unidade, mesmo para setores sem chamados (violação da regra #8 de banco/performance).
- **Correção:** uma única query agregada (`reorder()->distinct()->pluck(...)`) identifica os setores/unidades com chamados; só esses são paginados. Nos setores, o helper `setoresComChamados()` centraliza a lógica. Nas unidades, o índice `$idx` continua percorrendo a lista completa para preservar os nomes dos parâmetros de página (`u{idx}_pg`) usados na view - zero mudança visual/comportamental.

## 5. Achados da varredura complementar (corrigidos)

### 5.1 reCAPTCHA sem verificação SSL (regra #28)

- **Local:** `app/Http/Controllers/Controller.php`, método `recaptcha()` (chamada direta e fallback via proxy).
- **Vetor:** `CURLOPT_SSL_VERIFYPEER/HOST = false` permitia man-in-the-middle na validação do reCAPTCHA (um MITM poderia forjar `success: true` e burlar o desafio em formulários públicos).
- **Correção:** removidas as 4 diretivas; o cURL passa a validar o certificado de `www.google.com` normalmente (comportamento padrão).
- **Atenção operacional:** se o fallback via proxy `1.1.1.1:3128` falhar por inspeção TLS do proxy, tratar no proxy/CA do sistema, e não desabilitando a verificação.

### 5.2 XSS armazenado em notícias (regra #21)

- **Local:** `resources/views/paginas/slug.blade.php`
- **Vetor:** `{!! html_entity_decode($noticia->description) !!}` renderizava HTML de banco sem sanitização em página pública.
- **Correção:** troca por `{!! \App\Helpers\HtmlSanitizer::sanitize($noticia->description) !!}` (o sanitizador já decodifica entidades internamente) - mesmo padrão aplicado ao Fale Conosco no Relatório 04.
- **Risco residual aceito:** os campos `movie` e `code` da notícia continuam em `{!! !!}` por serem campos de embed administrativos (iframes de vídeo); sanitizá-los com o helper atual removeria os embeds. Ficam cobertos pela permissão do módulo de notícias e listados para avaliação com whitelist dedicada (ex.: `mews/purifier`).

### 5.3 Interpolação de direção de ordenação em `orderByRaw` (defesa em profundidade)

- **Locais:**
  - `app/Http/Services/Cultura/Credenciamento/MusicoService.php` (linha ~108)
  - `app/Http/Controllers/ProjetosProgramas/ProjetosProgramasController.php` (linha ~2865)
- **Situação:** ambos já tinham whitelist a montante (`in:asc,desc` na validação / `in_array` no controller), portanto **não eram exploráveis**.
- **Correção:** no `MusicoService`, whitelist explícita no ponto de uso (`=== 'asc' ? 'asc' : 'desc'`), eliminando a dependência da validação a montante. No `ProjetosProgramasController` a whitelist `in_array(..., ['asc','desc'], true)` já ocorre imediatamente antes da interpolação e foi mantida como está.

### 5.4 Padrões verificados sem ocorrências

Varredura em `app/`, `routes/` e `resources/views/`:

| Padrão | Resultado |
|---|---|
| `Blade::render`, `eval(`, `create_function` | Nenhuma ocorrência |
| `exec/shell_exec/system/passthru/proc_open/popen` | Nenhuma (só `curl_exec`, legítimo) |
| `unserialize(`, `extract($`, `assert(` | Nenhuma ocorrência |
| Escrita em `public_path()` (`move`, `file_put_contents`) | Nenhuma ocorrência |
| `md5/sha1` para senha | Nenhuma (usos existentes são chaves de cache) |
| `{!! html_entity_decode(...) !!}` | Só `slug.blade.php` (corrigido em 5.2) |
| `enable_php` do DomPDF | Continua `false` (guard adicionado) |

## 6. Testes de regressão de segurança

Criado `tests/Feature/SecurityGuardsTest.php` com 4 guards:

1. `config('dompdf.options.enable_php')` deve ser `false` (V1/V3).
2. Os 3 templates PDF do IndicadoresTI só podem conter `{!! !!}` para as variáveis SVG whitelisted (`$svgDonut`, `$svgGauge`, `$svgSetores`, `$svgBar`, `$d['svg']`) (V3).
3. As rotas de autocomplete devem manter `throttle:autocomplete` (V4).
4. `public/.htaccess` deve continuar bloqueando `phtml`/`phar`/`pht` (incidente #2).

Execução: `./vendor/bin/phpunit --filter SecurityGuardsTest` - 4 testes, 18 asserções, OK.

Observação: a suíte completa possui 5 falhas/erros **pré-existentes** e não relacionados (classes `ServicoDeSenha`/`ServicoDeContaPublica` ausentes em `AuthFlowsTest` e 2 asserções de domínio em `ProjetosProgramasValidationTest`/`CronogramasTest`).

## 7. Regras derivadas (rules.mdc)

Adicionadas à seção REGRAS DE SEGURANÇA:

- Regra #35: mascarar CPF e dados pessoais em respostas JSON de autocomplete/busca, mesmo em rotas autenticadas.
- Regra #36: toda mitigação de segurança relevante deve ganhar guard de regressão em `tests/Feature/SecurityGuardsTest.php`.

## 8. Pendências operacionais (fora deste workspace)

Permanecem em aberto no servidor de produção (Relatório 01 / P0 item 2 do relatório de incidentes):

- Varredura de artefatos `.ph*`/`.phar`/`.pht`/`.inc` em `public/` e conferência de hash de `public/index.php`.
- Rotação de credenciais (admin, SSH, banco, `.env`).
- Teste do `.htaccess` em produção: `403` para `.phtml`/`.php`.
- Monitoramento de integridade de `public/` (AIDE/inotify ou hash no deploy).
- Usuário do PHP-FPM sem escrita em `public/`; a médio prazo, avaliar isolamento do serviço/banco do Boletim Oficial (P2 item 7).
