# QA de segurança — Onda 4 (gate G4)

Revisão independente (`security-reviewer`) dos canais, uploads e assinatura documentados
na Onda 4, confrontados com `sucesu-cms/V2/`. Nenhum arquivo de `V2/` foi alterado.

**Escopo:** API das Unidades Regionais (`api.md` + `Routes/api.php`), NoticiasImport
(`spec.md`, `contrato-api.md`), Gallery (spec/UI), DigitalSignature + CertificadoDigital,
TrabalheConosco (currículos), AssocieSe (lead/e-mail). Índices parciais confrontados.

**Veredito:** reprovado como gate, com correções localizadas. SEC-G4-01 (alta) impede o
fechamento; SEC-G4-02/03/04/05/08 são condições de aprovação; SEC-G4-06/07 antes de usar
os checklists como critério.

## Achados

### SEC-G4-01 — Download de imagem da API regional é SSRF sem limite de resposta

**Severidade: alta.** `UnidadeRegionalNewsApiController.php:479-535` usa
`@file_get_contents($url, false, $context)` (`:499`), segue redirects (`:490-491`), desliga
TLS (`:494-495`) e é chamado para toda imagem inline do Markdown (`:411-437`, origem em
`MarkdownToTiptapConverter.php:435-440`). Só `cover_image_url` tem validação `url`
(`ApiNewsRequest.php:30`); não há allowlist de host, bloqueio de IP privado/loopback,
restrição a http(s) nem limite de bytes (o `GalleryConfig(..., 10240, ...)` em `:526` é
declarativo).

Documento (`api.md:57,380-381,487,695-696`) trata apenas como “SSL sem verificação/MITM”,
sem SSRF, `file://` ou DoS de memória. Impacto: token regional válido faz o servidor buscar
`127.0.0.1`, `169.254.169.254`, serviços internos ou ler arquivos locais; resposta grande é
carregada inteira.

Correção: seção “Riscos de download remoto” em `api.md` (e referência em `rotas.md`):
aceitar só http/https, resolver DNS e bloquear faixas privadas/loopback/link-local também
após redirect, reativar TLS, limitar bytes/tempo/redirects; MIME allowlist não é controle
de SSRF.

### SEC-G4-02 — `POST /associados/{id}` ignora `status`/`tags` isolados

**Severidade: média.** `UnidadeRegionalEmpresaApiController.php:295-302` retorna
`Nenhuma alteração detectada.` antes de atualizar `tags`/`status` (`:304-314`). `api.md:143-144`
e `:555-559` e `rotas.csv:8` prometem aplicação e `200`. Efeito: falso sucesso em campo de
estado.

Correção: documentar a ordem e o early return, acrescentar `PC` específico e corrigir o
índice; no código, mover os blocos para antes do `if (!$changed)`.

### SEC-G4-03 — Vínculo de associado nacional sem verificação de origem

**Severidade: média.** `linkExistingEmpresas` (`:426-447`) aceita qualquer
`AssociadoInstituicao::withTrashed()->find()` não arquivado, sem checar
`metadata.source_unidade_regional_id`/dono; `linkEmpresa` (`:468-496`) cria o pivô;
`updateAssociado` (`:253-260`) só verifica existência do vínculo, não `status='active'`.
`api.md:44-47,143-144,555-559` apresenta escopo “da própria unidade” e “vínculo ativo”.

Impacto: token regional que conheça um UUID nacional pode reivindicá-lo, alterar tags/dados
e arquivá-lo, afetando outras regionais.

Correção: declarar o comportamento real (vínculo existente, não ativo; qualquer UUID
nacional) e recomendar restringir a associados criados pela unidade/sem dono/allowlist e
exigir `status='active'`; corrigir índice.

### SEC-G4-04 — `CertificadoDigital`: `403` documentado, middleware responde `302`

**Severidade: média.** Rotas sob `CmsAuthenticate` → `V2PermissionMiddleware`; para `read`,
POST cai em `redirect()->back()` (`V2PermissionMiddleware.php:102-121`) ou 403 genérico em
AJAX. Os `v2_can` do controller (`:83-84,98-100,194-195,214-216,369-371`) só são
alcançados por quem já tem `write`. `spec.md:77,94,362,742-743,786-787` e
`PC-CERTIFICADODIGITAL-04` prometem `403`; `rotas.csv:4,6,8` repetem.

Correção: separar camadas e métodos (GET do controller × POST do middleware), reescrever o
PC e o índice; deixar claro que o controle efetivo de escrita é o middleware.

### SEC-G4-05 — `consumer` não confina o diretório de importação

**Severidade: média.** `NoticiasImportController.php:84-103` resolve `dir` antes de ler a
chave/consumidor e aplica `consumerAllows` só ao alvo; `resolveDir` (`:124-141`) aceita
qualquer caminho sob `storage/app/imports` ou `TMP`; o relatório expõe `source_url`,
`body_images` etc. (`NoticiasImportReport.php:97-116`). `spec.md:85,175-178` e
`contrato-api.md:303,406` não declaram o isolamento cruzado.

Impacto: chave de PE pode ler/aplicar o lote de RJ.

Correção: declarar que `dir` não é autenticado pelo `consumer`; recomendar diretório
derivado do consumidor ou allowlist por chave, com auditoria do `dir`.

### SEC-G4-06 — `401/403` do canal interno não passam pelo throttle

**Severidade: média.** `Routes/api.php:12-18` aplica `InternalImportKey` e `throttle.api`
na rota; `InternalApiAuth.php:42-58,82-96` retorna 401/403 antes do `$next`, e
`ThrottleApiToken` só conta depois (`:22-49`). Cada falha grava auditoria (`:218-232`).
`spec.md:454-455` apresenta a ordem como mitigação sem registrar a lacuna; `contrato-api.md:137,§2.2,§5.7`
idem.

Correção: documentar que falhas não consomem cota e recomendar throttle por IP antes da
autenticação (ou limiter de falhas) mantendo o throttle por token.

### SEC-G4-07 — `--ttl` inválido cria chave perpétua

**Severidade: baixa.** `NoticiasImportKeyCommand.php:64` faz `(int) $this->option('ttl')`;
`'abc'` vira `0` e `InternalApiKey.php:105` interpreta `0` como sem expiração.
`contrato-api.md:245,305,364` só documenta `0`; `PC-NOTICIASIMPORT-21` cobre 0/90, não o
inválido.

Correção: validar inteiro `>= 0` e abortar em valor inválido; documentar que só `--ttl=0` é
perpétuo e adicionar caso negativo.

### SEC-G4-08 — Currículos: documentação oferece disco público

**Severidade: média.** `TrabalheConosco/spec.md:559-565,733-735` recomenda “rota autenticada
ou disco `public`”. O código grava em `storage/app/resumes` (disco `local`,
`TrabalheConoscoController.php:93`) e a view usa `v2_storage_url`
(`applications/show.blade.php:64`), sem rota autenticada. Disco público exporia currículos
(nome, e-mail, telefone, LinkedIn) em `/storage/resumes/...` sem autenticação.

Correção: remover a opção de disco público; exigir rota autenticada com permissão, download
`Content-Disposition: attachment`, `nosniff`, sem cache, autorização por candidatura (ou URL
assinada curta) e política de retenção/consentimento.

## Documentos sem achado sobrevivente

- Gallery: riscos já documentados (rotas CMS sem auth, CSRF isento, SVG, `owner_module`);
  `PC-GALLERY-10` exige não reproduzir o estado legado.
- DigitalSignature: KDF sem custo/salt por registro, AES-CBC sem MAC, ausência de
  cadeia/CRL/OCSP/`verifySignature`, retenção de chave em soft delete, bypass de validade
  pelo suporte e `setSignatureAppearance` com argumentos trocados já constam.
- AssocieSe: ausência de throttle/captcha, escape de mensagens e retenção de dados já
  declarados; tratar como requisitos.
- Acessibilidade: sem superfície de segurança de servidor.
