# Épico de Segurança — Cabeçalhos HTTP + CSP estrito

> Novo plano **independente** (não acoplado ao EPIC de performance `pl-cc49` nem ao plano
> PWA `pl-12a6`). Foco exclusivo: implementar os **5 pontos de segurança** remanescentes.
> `PERF-1/2` e `SEC-6` (cache de API) estão **fora de escopo**.

Data: 2026-09-10 · Provider: Laravel 5.5 / PHP 7.2 / cPanel (Apache 2.4, EasyApache4)
Guia de referência: `.agents/skills/{web_security_guide, http_uri_guide, html_guide}`

## 1. Mandato e princípios
- Defesa em profundidade; Secure-by-default; HTTPS absoluto.
- CSP **nunca** com `'unsafe-inline'` em produção (guia) — por isso o caminho é
  Report-Only → coleta de violações → enforce por **hash** (sha384) dos inline
  (SEC-1f), coabitando com o cache de borda (A4).
- Headers de resposta via `.htaccess` (padrão do projeto); itens que exigem código
  (nonce, coletor, cookie) via Laravel.
- Sem `git commit` / `sd sync` automático (criação local de seeds apenas).

## 2. Escopo — 5 pontos
| # | Ponto | Atual | Alvo |
|---|-------|-------|------|
| SEC-1 | Content-Security-Policy | ausente | Report-Only → enforce por hash (sha384) dos inline + allowlist de 3rd-party |
| SEC-2 | CORP + COOP | ausente | `same-origin` (ambos) |
| SEC-3 | Cookie de sessão | `HttpOnly`+`SameSite=Lax` (via .htaccess) | `Secure`+`HttpOnly`+`SameSite=Lax` **nativo** |
| SEC-4 | `frame-ancestors` | só `X-Frame-Options: SAMEORIGIN` | `frame-ancestors 'self'` no CSP + manter XFO legado |
| SEC-5 | Redirect HTTPS | só HSTS (não redireciona 1ª visita) | `301` HTTP→HTTPS (aware `X-Forwarded-Proto`) |

## 3. Backlog detalhado

### Ponto 1 — SEC-1: Content-Security-Policy (prioridade 1)
Sub-tarefas (ordenadas):
- **SEC-1a — Coletor interno de violações** (`priority 2`)
  - Rota `POST /csp-report` (fora do grupo `check.site.manutention`, `throttle` leve,
    `csrf` desligado p/ ela) → `App\Http\Controllers\Portal\CspReportController@store`.
  - Aceita `application/csp-report` e `application/reports+json` (Reporting-Endpoints):
    lê `body`/`type`, anexa `request_id`, `url`, `ip`, `user-agent` e grava em
    `storage/logs/csp-violations.log` (formato JSON_lines; 1 linha/violação).
  - `.htaccess` H5: `Header always set Reporting-Endpoints "csp=\"https://%{HTTP_HOST}/csp-report\""`.
- **SEC-1b — Middleware `CspNonce`** (`priority 2`, bloqueia SEC-1c)
  - Grupo `web`, só `GET` de controllers `Portal\`; gera `nonce = Str::random(32)` por
    request, `View::share('cspNonce', $nonce)` + `request->attributes->set('cspNonce', …)`.
  - Na fase Report-Only: injeta o nonce mas **não** emite header enforce (os Blades já
    podem usá-lo). Header enforce entra só em SEC-1e.
- **SEC-2 / SEC-3 / SEC-5** podem rodar em paralelo a SEC-1a/1b (independentes).
- **SEC-1c — Migração portal-first dos scripts inline** (`priority 2`, bloqueado por SEC-1b)
  - Todo `<script>` inline dos templates **portal** vira `<script nonce="{{ $cspNonce }}">`.
  - Scripts de widget (charts/vlibras/recaptcha) → `<script src … nonce=…>` ou `defer`/
    `type=module` (ver `html_guide` §6/§8).
  - Ordem: `portal/layouts/partials/*` (pwa.blade.php, preconnect/speculationrules) →
    `modelo1..6/newsbit/layout/*` → `default/master.blade.php`. **Admin fica por último**
    (risco maior, fora do canário inicial).
  - Total hoje: 563 blocos `<script>` no projeto; portal é a fatia inicial do canário.
- **SEC-1d — SRI em CDN version-pinned** (`priority 3`, bloqueado por SEC-1c)
  - Fixar versão exata de `gstatic charts` e `cdn.jsdelivr chart.js`; adicionar
    `integrity="sha384-…" crossorigin="anonymous"`. recaptcha/vlibras (render dinâmico)
    ficam só no allowlist de host.
- **SEC-1e — Enforce CSP** (`priority 2`, bloqueado por SEC-1a/1c/1d e por **SEC-1f**)
  - Trocar `Content-Security-Policy-Report-Only` → `Content-Security-Policy` na
    **origem**. Abordagem adotada (**SEC-1f, #2**): política por **nonce por request**
    na origem (`script-src 'self' 'nonce-{R}' … 'unsafe-hashes'; style-src … 'nonce-{R}'
    'unsafe-hashes'`), injetado nos blocos `<script>`/`<style>` inline. A **borda
    (A4 / Multi-CDN) serve apenas `Report-Only`** (downgrade enforce→Report-Only no
    flip) — assim ela cacheia o corpo sem nunca bloquear, e não há *static nonce
    vulnerability* (o enforce só ocorre na origem, com nonce fresco por request).
  - Por que não hash (abordagem #1, descartada): o canário mostrou que `unsafe-hashes`
    não é honrado de forma confiável por todos os browsers para `script-src-attr`/
    `style-src-attr` (handlers `onclick=`/barra de acessibilidade e `style=""`
    continuavam blockeados), e libs vendor (fontawesome/vlibras) injetam `<script>`/
    `<style>` em runtime (indisponíveis para hash). Hash exigiria refactor de ~439
    handlers + centenas de `style=""` + correção de libs — inviável. Detalhes em
    `plan/2026-09-10-csp-enforce-edge-coexistence.md` (seed `SEC-1f`).
  - **ETag/304 (A2) continua funcionando na origem**: o nonce é injetado no corpo
    *depois* do `CacheControl` emitir o ETag, então o ETag não inclui o nonce e fica
    estável (o `applyToCachedResponse` do FPC re-injeta nonce fresco no HIT). O edge
    mantém seu próprio ETag estável na cópia cacheada.
  - Só após canário limpo (violações restantes = só 3rd-party conhecido e mapeado)
    **e** borda (A4) configurada para servir Report-Only (L1.3) no flip.

### Ponto 2 — SEC-2: CORP + COOP (`priority 2`)
- `.htaccess` H5 (cruas, sem `<IfModule>`):
  `Header always set Cross-Origin-Resource-Policy "same-origin"`
  `Header always set Cross-Origin-Opener-Policy "same-origin"`
- Baixo risco; habilita isolamento de origem (defesa contra ataques de leitura cruzada).

### Ponto 3 — SEC-3: Cookie de sessão (`priority 2`)
- `config/session.php`:
  `'secure' => env('SESSION_SECURE_COOKIE', true)` (hoje `false`)
  `'same_site' => env('SESSION_SAME_SITE', 'lax')` (hoje `null`)
- `.env` (cPanel Environment Variables): `SESSION_SECURE_COOKIE=true`, `SESSION_SAME_SITE=lax`.
- *Dependência:* exige HTTPS forçado (SEC-5). Manter o `Header edit SameSite=Lax` do
  `.htaccess` como backup até o deploy.

### Ponto 4 — SEC-4: frame-ancestors (`priority 3`, bloqueado por SEC-1e)
- Entregue dentro do header CSP de SEC-1e (`frame-ancestors 'self'`).
- Manter `X-Frame-Options: SAMEORIGIN` no `.htaccess` para browsers legados.
- (Opcional, se a política de embed for "nunca": `frame-ancestors 'none'`.)

### Ponto 5 — SEC-5: Redirect 301 HTTPS (`priority 2`)
- `.htaccess` (antes do front-controller), aware de proxy reverso:
  `RewriteCond %{HTTP:X-Forwarded-Proto} !https [NC]` (fallback `%{HTTPS} off`)
  `RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]`
- **Confirmar com ops** se a terminação SSL já é no Cloudflare/cPanel (evitar loop).
  Se já houver redirect em camada superior, esta regra é apenas defesa em profundidade.

## 4. Matriz de dependências
```
SEC-1a (coletor) ─┐
SEC-1b (nonce) ───┴─► SEC-1c (migração portal) ─┬─► SEC-1d (SRI) ─┐
                                                   │                              ├─► SEC-1e (enforce) ─► SEC-4 (frame-ancestors)
SEC-2 (CORP/COOP) ── independente                 │                              │
SEC-3 (cookie) ────── independente (req. SEC-5)   └──────────────────────────────┘
SEC-5 (HTTPS) ─────── independente (habilita SEC-3)
```
- SEC-4 bloqueado por SEC-1e.
- SEC-1e bloqueado por SEC-1a, SEC-1c, SEC-1d.

## 5. Critérios de aceitação / validação
- `curl -I /` mostra: `Cross-Origin-Resource-Policy: same-origin`,
  `Cross-Origin-Opener-Policy: same-origin`, e `Content-Security-Policy(-Report-Only)`
  com `frame-ancestors 'self'` + `base-uri 'none'` + `object-src 'none'`.
- `curl -I /sw.js` mantém `Cache-Control: public, max-age=0, must-revalidate` (L0.3b intacto).
- DevTools › Application › Cookies: `Secure` + `SameSite=Lax` no cookie de sessão.
- `curl -I http://<dominio>/` retorna `301` → `https://` (SEC-5).
- `storage/logs/csp-violations.log` recebe relatórios do Report-Only (validar com
  `curl` de um POST sintético ou DevTools).
- Lighthouse › Best Practices sobe; sem regressão em SEO/Acessibilidade.
- Canário: após SEC-1e, navegação portal + formulários (ouvidoria/contato) sem bloqueios
  de script; admin validado à parte.

## 6. Arquivos afetados
- `public/.htaccess` (H5 + redirect HTTPS + Reporting-Endpoints)
- `config/session.php` + `.env` (cookie)
- `app/Http/Middleware/CspNonce.php` (NOVO)
- `app/Http/Controllers/Portal/CspReportController.php` (NOVO) + `routes/web.php` (`/csp-report`)
- `resources/views/portal/layouts/**` (nonce nos `<script>` — migração portal-first)
- `app/Http/Kernel.php` (registrar `CspNonce` no grupo `web`)

## 7. Riscos
- **R1** (alto): 563 scripts inline — enforce prematuro quebra o site. Mitigação:
  Report-Only + nonce + migração portal-first antes do enforce.
- **R2** (médio): redirect HTTPS em loop atrás de proxy. Mitigação: `X-Forwarded-Proto`
  + confirmação com ops.
- **R3** (baixo): 3rd-party (vlibras/youtube/recaptcha) pode exigir ajuste de allowlist.
  Mitigação: coletor de violações orienta o ajuste.
