# QA de segurança — Onda 1: autenticação, RBAC e middlewares

Revisão independente (`security-reviewer`) dos documentos
`01_arquitetura/03-auth-rbac-sentinel.md` e `01_arquitetura/04-middlewares-seguranca.md`
contra o código real, com verificação cruzada de `01-boot-autodiscovery`, `02-roteamento`,
`06-tenancy-regional`, `07-controllers-core` e `09-cache-csp-minify-maintenance`.

**Método:** leitura integral dos dois documentos; confronto de cada citação com o código;
rastreio fonte → controle → sink para sessão CMS, canal IA, API interna, API regional,
feature flags e restrição regional; verificação de controles vizinhos (CSRF,
`TrustProxies`, ordem de middleware, guardas de controller, cache de página).
Fora de escopo: exploração ativa, alteração de código, execução de testes.

**Veredito:** reprovado como gate de segurança da Onda 1, com correções localizadas.
Condição de aprovação: corrigir SEC-G1-01 a SEC-G1-07; SEC-G1-08, -09 e -10 antes de usar
os checklists de paridade como critério de aceite.

## 1. Achados

### SEC-G1-01 — Rotas CMS do serviço Gallery sem autenticação e com CSRF isento

**Severidade: crítica.**

Evidência:

- `V2/Services/Gallery/Routes/cms.php:5` registra o grupo `cms/v2/gallery` sem
  `CmsAuthenticate`.
- `V2/Core/Providers/V2ServiceProvider.php:210-214` aplica às rotas CMS de serviço
  apenas o grupo `web`; a proteção teria de vir do arquivo de rotas.
- `V2/Services/Gallery/Controllers/GalleryController.php:33-36` não tem guarda de sessão,
  papel, `v2_can()` ou suporte; ações expostas em `Routes/cms.php:6-12` (`index`, `store`,
  `update`, `rotate`, `download`, `archive`, `destroy`).
- `app/Http/Middleware/VerifyCsrfToken.php:15-17` isenta `cms/v2/gallery/*`; `rotate`,
  `archive` e `destroy` são alcançáveis sem `_token`. `store` não é isento, mas o cookie
  `XSRF-TOKEN` é emitido em qualquer GET e não há autenticação.
- `01-boot-autodiscovery.md:272` reconhece a ausência de auth, mas afirma proteção por
  "cookie/sessão/CSRF" — falso nos subcaminhos isentos. `02-roteamento.md:258` só registra
  "Gallery não usa".

Impacto: cliente anônimo lista metadados, baixa arquivos, gira/arquiva/apaga mídia e envia
novos arquivos (inclusive `svg`, `GalleryController.php:88-89`). `[I]` SVG servido inline
pode virar XSS armazenado conforme os headers do storage.

Correção: documentar a combinação como superfície explorável em `03 §3.2`/`04 §3.3`,
corrigir `01-boot:272` e registrar que a correção recomendada é `CmsAuthenticate` no grupo
e remoção da isenção de CSRF.

### SEC-G1-02 — Guarda de produção do `CmsAiToken` é igualdade literal e não cobre `APP_ENV=prod`

**Severidade: alta.**

- `V2/Core/Middleware/CmsAiToken.php:52-54` compara `config('app.env') === 'production'`,
  case-sensitive.
- `docker-compose.yml:31` usa `APP_ENV: ${APP_ENV:-prod}`; `.env.example:10` usa `dev`;
  `config/app.php:32` define `production` apenas quando a env não existe.
- `04 §2.3` afirma "guarda de código, não de configuração"; `04:51,667-668` reforçam.
  `04 §3.3` reconhece `prod`, mas a ficha e a receita (`04:579-591`) seguem afirmativas.
- Com `APP_ENV=prod` e `V2_CMS_AI_ENABLED=true`, o canal autentica.

Correção: trocar "impossível ativar em produção" por "só bloqueia `production` exato";
declarar que a proteção efetiva é `V2_CMS_AI_ENABLED=false` + token ausente + allow-list
de IP; incluir teste negativo com `prod`.

### SEC-G1-03 — Allow-list por IP é spoofável com `TrustProxies = '*'`

**Severidade: alta.**

- `app/Http/Middleware/TrustProxies.php:15,23-27` confia em qualquer proxy e aceita
  `Forwarded`/`X-Forwarded-For`.
- `$request->ip()` alimenta a allow-list do canal IA (`CmsAiToken.php:68,136-147`), da API
  interna (`InternalApiAuth.php:80-83`, `InternalApiKey.php:154-163`) e o throttle por IP
  (`ThrottleApiToken.php:33`).
- `04` apresenta `allowed_ips` como controle sem ressalva (`RN-CORE-AUTH-17:453`,
  anti-uso `:669`).

Correção: documentar a precondição de borda (proxies reais e remoção de headers do
cliente) e classificar IP como defesa secundária.

### SEC-G1-04 — Reset de tema do CMS sem guarda de suporte

**Severidade: média.**

- `SupportConfigs/ModuleProvider.php:87` declara `active_pattern` como string com `|`;
  `Request::is()` não separa por `|`, então o padrão nunca casa e
  `V2PermissionMiddleware.php:75-77` libera.
- `SupportConfigs/Routes/cms.php:61-64` expõe `POST /cms/infra/user-configs/cms-theme/reset`
  para `UserConfigsCmsController@resetCmsTheme`.
- `StructuralLandingEditor/Controllers/UserConfigsCmsController.php:38-45` não tem guarda;
  os demais controllers do grupo chamam `authorizeSupportAccess()`.
- Efeito: sobrescreve `v2_cms_theme_configs`, incluindo marca, logo e favicon
  (`SystemMaintenanceService.php:292-303`).

Correção: separar as rotas com defesa de controller das sem defesa; recomendar guarda e
`active_pattern` em array; registrar no doc do módulo (Onda 3).

### SEC-G1-05 — `consumerAllows()` tem chamador; `04` afirma o contrário com `[C]`

**Severidade: média.**

- `04:469-471` e `LG-CORE-AUTH-12` dizem "nenhum middleware o chama; não há enforcement".
- `InternalApiKey.php:212-219` é chamado em
  `Support/NoticiasImport/Controllers/NoticiasImportController.php:98-103`, com 403
  `consumer_mismatch`.
- A chave estática é vinculada como `null` (`InternalApiAuth.php:110-116`), pulando a
  checagem e caindo no default `'PE'` (`NoticiasImportController.php:94-95`).

Correção: corrigir `§1.4`, `§3.3` e `LG-12`: middleware não valida `consumer`; o
controller de importação valida; a chave estática ignora o controle.

### SEC-G1-06 — `03 §3.2` diz que qualquer role acessa SupportConfigs por URL direta

**Severidade: média.**

- `03:468-470` atribui a defesa ao middleware e afirma acesso livre.
- `SupportConfigsCmsController.php:47-53` chama `authorizeSupportAccess()` em todas as
  actions (`:61,83,103,135,167,192,218,241,271,294`).
- `SleCmsController.php:31-36` protege o base path não coberto.

Correção: distinguir (a) RBAC/grupo não aplicado; (b) defesa por action em
`SupportConfigsCmsController` e `SleCmsController`; (c) ausência de guarda apenas em
`user-configs/cms-theme/reset`. Exigir que novos controllers do grupo repitam a guarda.

### SEC-G1-07 — Dashboard com `v2_role_id = null` responde 200, não 302

**Severidade: média.**

- `03:105` promete 302 do dashboard.
- `CmsAuthenticate.php:30,44` só exige role Sentinel e delega ao RBAC.
- Nenhum `active_pattern` cobre `cms`; `V2PermissionMiddleware.php:75-77` libera.
- `03 §2.3` e `07-controllers-core.md:73,99` descrevem corretamente o fail-open.

Correção: substituir por "sidebar vazia; `/cms`, `/cms/search` e `/cms/conexao-tags`
acessíveis; rotas de módulo 302"; adicionar caso ao checklist.

### SEC-G1-08 — `last_login` é escrito pelo Sentinel, antes dos gates

**Severidade: baixa (paridade/auditoria).**

- `03:139-142,482-483` e `LG-04` afirmam que a coluna nunca é atualizada.
- `vendor/cartalyst/sentinel/src/Sentinel.php:289-330,487-499` e
  `Users/IlluminateUserRepository.php:134-141` gravam `last_login` no login.
- Consumidores: `UserManagementService.php:59,105` e
  `DashboardStatsService.php:474-480`.

Correção: documentar escrita pelo pacote, antes dos gates de role/ativação.

### SEC-G1-09 — Módulo desativado em rota CMS retorna 404, não 302

**Severidade: baixa.**

- `ModuleAccessMiddleware.php:56-58` só redireciona quando `getPrefix()` é exatamente
  `cms`; senão `abort(404, 'Modulo desativado.')` (`:62`).
- Rotas de módulo usam prefixos compostos (`cms/noticias`, `cms/infra/support-configs`).
- `04:708` (`PC-CORE-AUTH-24`) promete 302.

Correção: ajustar PC-24 e a tabela de efeitos; corrigir a explicação de `getPrefix()`.

### SEC-G1-10 — Versão do Sentinel divergente

**Severidade: baixa.**

- `03:706` e `04:690` citam 2.0.16; `composer.lock:65-66` resolve 2.0.18.

Correção: atualizar e citar os valores de
`vendor/cartalyst/sentinel/src/config/config.php`.

### SEC-G1-11 — Login AJAX retorna 500 antes do handler de flash

**Severidade: baixa.**

- `app/Exceptions/Handler.php:55-60` trata qualquer exceção AJAX com 500 JSON antes dos
  `instanceof` de `ThrottlingException` e `NotActivatedException` (`:63-71`).
- `03:168-170` promete flash + redirect sem qualificar.

Correção: qualificar "em requisições não-AJAX".

### SEC-G1-12 — Canal IA não revalida role/ativação e impersona o suporte por default

**Severidade: média.**

- `CmsAiToken.php:80-90,192-198` resolve por e-mail e chama `Sentinel::setUser()` sem
  checar roles ou ativação.
- `config/v2.php:68` faz o alvo default cair em `V2_SUPPORT_EMAIL`, que tem bypass total
  (`V2PermissionMiddleware.php:70`, `RegionalScopeService.php:171-176`).
- Token fixo, sem TTL/rotação, com CSRF dispensado; um GET em `/cms/logout` encerra a
  sessão do usuário impersonado (`LoginCmsController.php:90-97`).

Correção: declarar o raio de impacto, recomendar usuário dedicado de menor privilégio e
`V2_CMS_AI_ENABLED=false` por default.

### SEC-G1-13 — Token por query pode vazar em log de outro middleware

**Severidade: baixa.**

- O logger do `CmsAiToken` usa `path()` (`:227-239`), mas
  `V2PermissionMiddleware.php:57,90,113` registra `fullUrl()`.
- Com `V2_CMS_AI_ALLOW_QUERY=true` e negação de RBAC, o `v2_ai_token` aparece no log.

Correção: limitar a garantia "nunca logado" ao logger do middleware; reforçar
`allow_query=false`.

### SEC-G1-14 — Canal Bearer por Unidade Regional ausente do inventário

**Severidade: baixa (cobertura).**

- `04:35-38` enumera cinco canais, omitindo o Bearer das rotas regionais
  (`UnidadeRegionalNewsApiController.php:447-467`,
  `UnidadeRegionalEmpresaApiController.php:347-367`; token `asc_`, sha256, expiração em
  `UnidadeRegional.php:255-285`).
- O TTL lê `config('v2.unidade_regional.api_token_ttl_days', 365)`, mas a seção não existe
  em `config/v2.php`; o valor é fixo em 365. `MODULE.md:39` afirma o contrário.
- GET/DELETE da API regional não declaram `throttle.api`
  (`Routes/api.php:4-6,15-16,26-30`).

Correção: adicionar o canal ao inventário ou cross-reference explícito em
`06-tenancy-regional`, com hash/TTL/revogação.

## 2. Verificações que não geraram achado

| Afirmação | Resultado |
|---|---|
| `users.permissions_v2` é coluna morta | confirmado (nenhum leitor/escritor em `V2/`) |
| `super-admin` não é semeado; seed cria `admin` | confirmado (`database/seeds/DatabaseSeeder.php:28-53`) |
| `read` = GET/HEAD | confirmado (`V2PermissionMiddleware.php:102-105`) |
| `active_pattern` com `|` nunca casa | confirmado |
| Chave estática ignora escopo/TTL/IP/revogação | confirmado (`InternalApiAuth.php:110-116`) |
| `allowed_ips` vazio = qualquer IP | confirmado |
| Throttle por 12 caracteres/IP | confirmado (`ThrottleApiToken.php:29-34`) |
| `markUsed()` toca `updated_at` | confirmado (`InternalApiKey.php:226-230`) |
| `ModuleFlagService` default ativo e cache estático | confirmado |
| CMS não é cacheado em HTML | confirmado (`PageCacheMiddleware.php:15-22`) |
| Sessão regenerada no login; logout invalida | confirmado |
| Comparações de segredo em tempo constante | confirmado (`hash_equals`) |

## 3. Ações de correção por documento

| Documento | Correções |
|---|---|
| `03-auth-rbac-sentinel.md` | G1-05, G1-06, G1-07, G1-08, G1-10, G1-11, G1-12 (impacto), G1-01 (regra de rotas de serviço) |
| `04-middlewares-seguranca.md` | G1-01, G1-02, G1-03, G1-05, G1-09, G1-10, G1-12, G1-13, G1-14 |
| `01-boot-autodiscovery.md` | G1-01 (afirmação de CSRF em Gallery) |
| `02-roteamento.md` | G1-01 (registro da ausência de auth) |
| `07-controllers-core.md` | G1-07 (conferir efeito do dashboard) |
| `09-cache-csp-minify-maintenance.md` | G1-03 (contexto de `TrustProxies` além do cache) |
| unidade `SVC-GALLERY` (Onda 2) | G1-01 como efeito/particularidade de segurança |

## 4. Lacunas abertas

`LG-SEC-G1-01` Gallery sem auth + CSRF isento; `LG-SEC-G1-02` reset de tema sem guarda;
`LG-SEC-G1-03` `TrustProxies` invalida allow-lists; `LG-SEC-G1-04` canal regional ausente
e TTL hardcoded; `LG-SEC-G1-05` `last_login` escrito antes dos gates.
