# 10 — Achados de revisão e patches recomendados

> **Natureza deste capítulo:** revisão crítica do snapshot `ed413d8` confrontada
> com o código, os documentos e o comportamento do Apache/PHP. **Nenhum patch
> abaixo foi aplicado ao repositório `v6-hardening`**; são recomendações para
> quem for evoluir a aplicação. O que está em produção deve ser validado no host
> real (`phpinfo`, `curl`, logs) antes de qualquer mudança.
>
> Legenda de severidade:
> **P0** = quebra/segurança iminente; **P1** = impacto operacional relevante;
> **P2** = robustez/manutenção; **P3** = cosmético/documentação.

---

## 1. Matriz de prioridade

| ID | Severidade | Achado | Estado atual | Impacto se não corrigir |
|---|---|---|---|---|
| **F-01** | **P0 quando CSP ligado** | Ordem `CspNonce` × `CacheControl` quebra ETag/304; FPC serve nonce velho; partials com `nonce=""` | CSP desligado (latente) | Enforce bloqueia scripts; 304 nunca dispara; header/body divergem |
| **F-02** | **P1** | `immutable`/`Expires 1 year` em uploads não versionados (`/storage`, `/img`) | Ativo | Imagem/CSS antigo por até 1 ano em navegador/CDN |
| **F-03** | **P2** | `h7https` nunca é definido; `SameSite` anexado sem checar duplicata | Ativo | Cookie com `SameSite` duplicado; linha `env=h7https` inerte |
| **F-04** | **P1** | `composer.lock` ausente e ignorado | Ativo | Instalações não reproduzíveis; versões flutuantes |
| **F-05** | **P1** | APCu: status conflitante e comentário errado no template | Ambíguo | Deploy/autoload achando que há cache quando não há |
| **F-06** | **P1** | Reset de OPcache não automatizado no deploy | Ativo | Código antigo em memória após deploy |
| **F-07** | **P2** | `.gitignore` ignora `.htaccess`, `config.json`, `composer.lock` | Ativo | Novos `.htaccess`/config não versionados; perda de rastreabilidade |
| **F-08** | **P2** | `deploy.sh` semeia sempre, `.env.example` com `DB_HOST=postgres`, `PAGE_CACHE_TTL=3600` | Ativo | Deploy falha/duplica seed; cache 1h em vez de 5min |
| **F-09** | **P2** | Log de violações CSP sem rotação/limite | Latente | Disco cheio se CSP for ligado |
| **F-10** | **P3** | Comentário do `CacheControl` diz “global”, mas está no grupo `web` | Ativo | Confusão de manutenção |
| **F-11** | **P2** | `PageCache::shouldCache` depende de ordem frágil | Ativo | Se a ordem mudar, FPC pode gravar resposta com cookie |
| **F-12** | **P2** | `sitemap.xml`/`robots.txt` estáticos legados e rewrite frágil | Ativo | SEO incorreto se a ordem do `.htaccess` mudar |
| **F-13** | **P2** | `CacheControl` e `CspNonce` atuam em respostas não-HTML | Ativo | Cache público de `manifest`/XML por 300s; header CSP em texto |
| **F-14** | **P3** | `HtmlMinify` usa regex não aninhada para blocos sensíveis | Ativo | Risco em `<pre>`/`<svg>` aninhados |
| **F-15** | **P3** | `PageCache` registry duplica chaves e regrava `forever` a cada MISS | Ativo | Crescimento lento/overhead |
| **F-16** | **P2** | H8 não bloqueia `.htaccess`, `.htpasswd`, `artisan`, `storage/logs` | Ativo | Defesa em profundidade incompleta |
| **F-17** | **P1** | CI/CD não roda `composer prod` nem reset de OPcache | Ativo | Deploy via pipeline pode não ativar config/route cache |

---

## 2. F-01 — CSP × ETag × FPC (o achado mais importante)

### 2.1 Evidência

O grupo `web` do `Kernel` está nesta ordem:

```php
'web' => [
    \App\Http\Middleware\ServerTiming::class,
    \App\Http\Middleware\PageCache::class,
    \App\Http\Middleware\CacheControl::class,
    \App\Http\Middleware\HtmlMinify::class,
    \App\Http\Middleware\PreloadCritical::class,
    \App\Http\Middleware\PictureTransform::class,
    \App\Http\Middleware\CspNonce::class, // <-- depois de CacheControl
    // ...
],
```

No pipeline do Laravel, a **resposta** percorre o array em ordem inversa. Logo,
no outbound, `CspNonce` roda **antes** de `CacheControl`:

```text
request  : ServerTiming -> PageCache -> CacheControl -> ... -> CspNonce -> ...
response : ... -> CspNonce -> PictureTransform -> ... -> CacheControl -> PageCache -> ServerTiming
```

Consequências com `config('csp.enabled') = true`:

1. `CspNonce` injeta `nonce` nos `<script>/<style>` inline;
2. `CacheControl` calcula `ETag = md5(corpo)` **incluindo o nonce**;
3. o nonce é aleatório por request → o ETag muda a cada request → `304` nunca
   dispara (regressão de A2);
4. no MISS do FPC, `PageCache` grava o corpo **já com o nonce** (porque roda
   depois de `CspNonce` no outbound);
5. no HIT, `CspNonce::applyToCachedResponse()` chama o injetor, mas ele **ignora
   tags que já têm `nonce=`** — o nonce gravado não é substituído;
6. o header CSP usa um nonce novo, o corpo usa o nonce antigo → header/body
   divergem; sob enforce, os scripts são bloqueados.

Além disso, os partials `pwa.blade.php` e `preconnect.blade.php` usam
`nonce="{{ $cspNonce ?? '' }}"`. O `CspNonce` **não** faz
`View::share('cspNonce', $nonce)`, então a variável não existe e o atributo sai
como `nonce=""`. Como o injetor ignora qualquer tag com `nonce=`, esses dois
scripts ficam com nonce vazio mesmo com CSP ligado.

> **Por que os testes não pegam:** `CspNonceTest` chama `applyToCachedResponse()`
> sobre um corpo **sem** nonce; não testa o corpo que o `PageCache` realmente
> gravou (com nonce). `CacheControlTest` não executa o `CspNonce`. O teste de HIT
> do FPC também não liga CSP.

### 2.2 Patch recomendado (não aplicado)

**a) Ordem:** mover `CspNonce` para **antes** de `CacheControl` no array, para
que no outbound ele rode **depois** do ETag:

```php
'web' => [
    \App\Http\Middleware\ServerTiming::class,
    \App\Http\Middleware\PageCache::class,
    \App\Http\Middleware\CspNonce::class,   // outbound: depois de CacheControl
    \App\Http\Middleware\CacheControl::class,
    \App\Http\Middleware\HtmlMinify::class,
    \App\Http\Middleware\PreloadCritical::class,
    \App\Http\Middleware\PictureTransform::class,
    // ...
],
```

> A ordem relativa a `HtmlMinify`/`PictureTransform` também importa: o nonce deve
> ser injetado no HTML final que será cacheado. Valide HIT/MISS e comparar o
> header com todos os `nonce="..."` do corpo.

**b) Injetor:** substituir nonce existente em vez de pular a tag:

```php
// CspNonce::injectNonceIntoHtml()
$content = preg_replace_callback('/<script\b([^>]*)>/i', function ($m) use ($nonce) {
    $attrs = $m[1];
    if (preg_match('/\bsrc\s*=/i', $attrs)) {
        return $m[0];
    }
    if (preg_match('/\bnonce\s*=\s*("[^"]*"|\'[^\']*\')/i', $attrs)) {
        $attrs = preg_replace('/\bnonce\s*=\s*("[^"]*"|\'[^\']*\')/i', '', $attrs);
    }
    return '<script' . $attrs . ' nonce="' . $nonce . '">';
}, $content);
```

Faça o mesmo para `<style>`. Alternativa: **remover** qualquer `nonce` do corpo
antes de gravar no FPC e reaplicar no HIT.

**c) Partials:** remover `nonce="{{ $cspNonce ?? '' }}"` ou compartilhar a
variável de fato. Compartilhar **não basta** se o header gerar outro nonce:
gere o nonce uma única vez e reutilize-o no `View::share` e na política.

```php
// CspNonce::handle(), antes de $next
$nonce = Str::random(32);
View::share('cspNonce', $nonce);
$response = $next($request);
$this->applyHeadersAndNonce($response, $request, $nonce);
```

O injetor deve aceitar a tag já preenchida (ou substituir pelo mesmo valor).

**d) Testes de regressão:**

```php
// 1) corpo do MISS do FPC, com CSP ligado, gera ETag estável entre requests
// 2) HIT do FPC: nonce do header == todos os nonce do corpo
// 3) partial com nonce="" é corrigido para o nonce do header
```

**e) Até o patch:** manter `csp.enabled=false` (estado atual) ou adotar o fallback
#2 do plano SEC-1f (não cachear HTML na borda). **Não** ligar enforce antes de
resolver os itens (a)–(c).

---

## 3. F-02 — `immutable` e `Expires` amplos demais

### 3.1 Evidência

```apache
<FilesMatch "\.(css|js|jpg|jpeg|png|webp|avif|gif|svg|woff2?|ttf|otf|ico)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
```

O `FilesMatch` casa **qualquer** arquivo com essas extensões sob `public/`:

- `public/assets/**` — versionado por hash/path; `immutable` é correto;
- `public/storage/**` — symlink para `storage/app/public`; uploads do CMS podem
  ser substituídos mantendo o mesmo nome;
- `public/img/**` — logos/favicons legados, geralmente sem hash;
- `public/css`, `public/js` legados — alguns sem versão.

`ExpiresByType ... "access plus 1 year"` no `[H9]` tem o mesmo efeito.

### 3.2 Impacto

Se o CMS substituir `logo.png` no mesmo caminho, o navegador/CDN pode continuar
servindo a versão antiga por 1 ano. O `ETag`/`Last-Modified` não ajuda porque
`immutable` instrui o cliente a não revalidar.

### 3.3 Patch recomendado

Escopar por path e usar TTL curto/revalidação para uploads. Exemplo com
`<If>` (Apache 2.4):

```apache
# Assets versionados: immutable é seguro
<If "%{REQUEST_URI} =~ m#^/assets/#">
    <FilesMatch "\.(css|js|jpg|jpeg|png|webp|avif|gif|svg|woff2?|ttf|otf|ico)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
</If>

# Uploads do CMS: mesmo path pode ser reutilizado
<If "%{REQUEST_URI} =~ m#^/storage/#">
    <FilesMatch "\.(jpg|jpeg|png|webp|avif|gif|svg)$">
        Header set Cache-Control "public, max-age=300, must-revalidate"
    </FilesMatch>
</If>

# Favicon/logo legados sem hash
<If "%{REQUEST_URI} =~ m#^/img/#">
    <FilesMatch "\.(jpg|jpeg|png|webp|avif|gif|svg|ico)$">
        Header set Cache-Control "public, max-age=3600, must-revalidate"
    </FilesMatch>
</If>
```

E remover/escopar o `ExpiresByType` de 1 ano para uploads. Validar a sintaxe
`<If>` no Apache alvo; se `AllowOverride` não permitir, aplicar no vhost.

> **Alternativa mais simples:** versionar todos os uploads no caminho
> (`/storage/configuracoes/<hash>-logo.png`) e manter `immutable`. Isso exige
> mudança no CMS e migração dos arquivos existentes.

---

## 4. F-03 — `h7https` inexistente e `SameSite` duplicado

### 4.1 Evidência

```apache
Header always edit Set-Cookie "^(.+)$" "$1; SameSite=Lax" env=h7https
Header always edit Set-Cookie "^(.+)$" "$1; SameSite=Lax" env=!h7https
```

Não há `SetEnvIf`/`SetEnv` definindo `h7https` no snapshot. A primeira linha
nunca casa; a segunda casa sempre. Além disso, o regex não verifica se o cookie
já tem `SameSite`, então pode gerar:

```text
Set-Cookie: laravel_session=...; path=/; secure; httponly; samesite=lax; SameSite=Lax
```

### 4.2 Impacto

Baixo (navegadores toleram atributo duplicado), mas é ruído e pode confundir
auditoria. A fonte autoritativa é `config/session.php`.

### 4.3 Patch recomendado

**Opção A (preferida):** remover as duas linhas do `.htaccess` e confiar em
`config/session.php`:

```php
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => env('SESSION_SAME_SITE', 'lax'),
```

**Opção B:** manter apenas uma linha com negative lookahead:

```apache
Header always edit Set-Cookie "^(?!.*SameSite)(.+)$" "$1; SameSite=Lax"
```

Validar no Apache alvo (PCRE lookahead) e testar cookies de sessão, XSRF e
`portal_csrf`.

---

## 5. F-04 — `composer.lock` ausente

### 5.1 Evidência

```bash
ls composer.lock   # No such file
git check-ignore -v composer.lock
# .gitignore:141:composer.lock    composer.lock
```

O projeto fixa `php: 7.2.34` e versões `^`/`*` em várias dependências. Sem lock,
`composer install` resolve versões novas a cada deploy (dentro das constraints),
o que quebra reprodutibilidade e pode introduzir regressões.

### 5.2 Patch recomendado

1. Em um ambiente com PHP 7.2.34/Composer compatível, rodar:
   ```bash
   composer update --lock --no-scripts
   composer validate --strict
   ```
2. Remover `composer.lock` do `.gitignore` e versioná-lo.
3. No deploy, usar `composer install --no-dev --prefer-dist --no-interaction`
   (sem `update`).
4. Se o lock não puder ser gerado agora, **congelar** as versões no
   `composer.json` (sem `^`/`*`) e documentar.

> O `composer.json` já tem `"platform": {"php": "7.2.34"}` e
> `"sort-packages": true`; o lock é o que falta para reproducibilidade.

---

## 6. F-05 — APCu: status conflitante e comentário incorreto

### 6.1 Evidência

- `WHM.md` §C.1: APCu **ativo** via `pecl install apcu`.
- `WHM.md` tabela final: **N/A**.
- `correcao-htaccess-whm-performance-l0.md`: **não aplicado**.
- `whm.php.ini.recomended`: comentário diz para instalar
  `ea-php72-php-apcu` pelo WHM EasyApache 4.
- `composer.json`: `"apcu-autoloader": true`.

### 6.2 Impacto

Operação pode assumir que o autoloader está em APCu quando não está, ou
descomentar `apcu.enabled` sem a extensão e gerar warnings. O deploy não quebra
(degradação graciosa), mas a performance esperada não se realiza.

### 6.3 Patch recomendado

1. Validar no host:
   ```bash
   /opt/cpanel/ea-php72/root/usr/bin/php -m | grep -i '^apcu$'
   /opt/cpanel/ea-php72/root/usr/bin/php -r "var_dump(extension_loaded('apcu'));"
   ```
2. Se ausente, decidir: instalar via `pecl` (build manual) **ou** aceitar sem
   APCu.
3. Corrigir o comentário do template (o RPM não existe neste host) — mantendo a
   cópia literal em `arquivos/` apenas como snapshot.
4. Após rebuild do EA4, revalidar `apcu.so` e o `zzz-apcu.ini`.
5. Rodar `composer dump-autoload -v` e conferir “with APCu”.

---

## 7. F-06 — Reset de OPcache não automatizado

### 7.1 Evidência

`deploy.sh` chama `composer prod`, que termina com:

```json
"echo 'Reinicie o PHP-FPM no WHM para aplicar as atualizações'"
```

Não há `opcache_reset()`/restart no script. Com
`opcache.validate_timestamps=0`, o FPM continua servindo o código antigo.

### 7.2 Patch recomendado

**No deploy (root/WHM):**

```bash
# Opção confiável: restart/reload do FPM
whmapi1 restart_service service=php-fpm
# ou
/scripts/restartsrv_apache_php_fpm
```

**Alternativa app-side (endpoint autenticado, SAPI FPM):**

```php
// routes/web.php (temporário, protegido por token/allowlist)
Route::get('/_ops/opcache-reset', function () {
    abort_unless(app()->environment('production') && request()->header('X-Ops-Token') === config('ops.token'), 404);
    opcache_reset();
    return response('ok');
});
```

> **Não** use `php -r 'opcache_reset();'` no CLI esperando limpar o FPM: são
> memórias separadas. `touch public/index.php` não é garantido com
> `validate_timestamps=0`.

**No `composer prod`:** o `echo` pode ser mantido, mas o pipeline de deploy deve
falhar se o reset não ocorrer. Exemplo de verificação:

```bash
# após restart, confirme que o OPcache está ativo e sem código antigo
/opt/cpanel/ea-php72/root/usr/bin/php -i | grep -E 'opcache.enable|opcache.validate_timestamps'
curl -sI https://SEU_HOST/ | grep -i x-hardening-version
```

---

## 8. F-07 — `.gitignore` e arquivos de infraestrutura

### 8.1 Evidência

```text
.gitignore:141:composer.lock
.gitignore:142:config.json
.gitignore:...:.htaccess
```

`public/.htaccess` e os `.htaccess` de upload estão **tracked** (force-add
histórico), mas novos `.htaccess` não serão versionados por padrão. O
`config.json` é gerado em runtime pelo CMS; perdê-lo não é problema, mas ele
contém configuração efetiva (app/mail/recaptcha) e não deve ser commitado.

### 8.2 Patch recomendado

- Manter `config.json` fora do Git (é runtime), mas **backup** no deploy.
- Para `.htaccess` de infraestrutura, usar exceções explícitas:
  ```gitignore
  .htaccess
  !public/.htaccess
  !public/img/.htaccess
  !storage/app/public/.htaccess
  !storage/app/public/img/.htaccess
  !storage/img/.htaccess
  ```
- Versionar `composer.lock` (ver F-04).
- Documentar no README quais arquivos são force-add.

---

## 9. F-08 — `deploy.sh` e `.env.example`

### 9.1 Evidência

- `deploy.sh` executa `php artisan db:seed --class=ExtrasSeeder` **sempre**, sem
  `--force` e sem checar idempotência.
- `.env.example` tem `DB_HOST=postgres` (Docker) e `PAGE_CACHE_TTL=3600`.
- `composer prod` roda `migrate --force` e `images:redigest-webp --limit=100000`
  no deploy.

### 9.2 Impacto

- Em cPanel, `DB_HOST=postgres` não resolve; o `.env` de produção precisa ser
  criado antes.
- Seed repetido pode duplicar dados se o seeder não for idempotente.
- `PAGE_CACHE_TTL=3600` deixa o HTML 1h na origem, enquanto a borda usa 300s.
- `images:redigest-webp --limit=100000` pode atrasar o deploy.

### 9.3 Patch recomendado

```bash
# deploy.sh (trecho)
if [ ! -f .env ]; then
  echo "ERRO: .env de producao ausente; crie antes do deploy." >&2
  exit 1
fi

# seed opcional e explicito
if [ "${RUN_EXTRAS_SEEDER:-0}" = "1" ]; then
  php artisan db:seed --class=ExtrasSeeder --force
fi
```

```dotenv
# .env.example (produção cPanel)
DB_HOST=127.0.0.1
PAGE_CACHE_TTL=300
```

E reduzir o `--limit` do `images:redigest-webp` no deploy (ex.: 300) ou deixar
apenas para o scheduler.

---

## 10. F-09 — Log de violações CSP sem limite

### 10.1 Evidência

`CspReportController` faz `file_put_contents(..., FILE_APPEND | LOCK_EX)` em
`storage/logs/csp-violations.log`, sem rotação, tamanho máximo ou rate limit
além do `throttle:120,1` da rota.

### 10.2 Impacto

Se o CSP for ligado e houver muitas violações (libs runtime, extensões de
navegador), o arquivo pode crescer e encher o disco da conta. O conteúdo também
inclui `User-Agent`/`Referer` (dados pessoais) sem política de retenção.

### 10.3 Patch recomendado

- Rotacionar por tamanho/dia (logrotate ou `storage/logs/csp-YYYYMMDD.log`).
- Limitar o corpo aceito (ex.: 16 KB) e validar `Content-Type`.
- Anonimizar IP (hash/truncar) após N dias.
- Adicionar um teto de linhas/bytes e `Log::warning` quando exceder.
- Nunca servir esse arquivo publicamente (já está fora de `public/`).

---

## 11. F-10 — Comentário do `CacheControl` diz “global”

### 11.1 Evidência

O docblock do `CacheControl` afirma:

> “Middleware GLOBAL (registrado em Kernel::$middleware...)”

Mas o `Kernel` o registra no grupo `web` (`$middlewareGroups['web']`), não em
`$middleware`. O comportamento observado é o do grupo `web` (rota resolvida).

### 11.2 Impacto

Apenas manutenção: alguém pode mover o middleware para `$middleware` achando que
é o lugar correto, e então `$request->route()` será `null` (o guard por
`Portal\` deixa de funcionar).

### 11.3 Patch recomendado

Corrigir o comentário para “middleware do grupo `web`” e manter o registro atual.

---

## 12. F-11 — `PageCache::shouldCache` depende da ordem

### 12.1 Evidência

```php
if ($response->headers->get('Set-Cookie') !== null) {
    return false;
}
```

O `PageCache` está antes do `CacheControl` no array; no outbound, o
`CacheControl` remove o cookie **antes** de o `PageCache` avaliar a resposta.
Funciona hoje, mas é frágil: se o `CacheControl` for movido para depois do
`PageCache` no outbound (ou removido), o FPC pode gravar uma resposta com
cookie.

### 12.2 Patch recomendado

Adicionar uma checagem defensiva também no array de cookies:

```php
if ($response->headers->get('Set-Cookie') !== null
    || count($response->headers->getCookies()) > 0) {
    return false;
}
```

E um teste que monte a resposta com cookie e verifique que o FPC não grava.

---

## 13. F-12 — `sitemap.xml`/`robots.txt` legados

### 13.1 Evidência

- `public/sitemap.xml` (5.132 B) ainda está versionado, com `lastmod` de 2023 e
  URLs de outros domínios.
- `public/robots.txt` estático não tem `Sitemap:`.
- O `.htaccess` reescreve `^robots\.txt$` e `^sitemap\.xml$` para `index.php`
  **antes** do bloco `-f`; as rotas dinâmicas existem e os controllers foram
  reescritos.
- A ordem é correta hoje, mas se alguém mover os rewrites para depois do
  `-f`, o estático volta a vencer.

### 13.2 Impacto

SEO: crawlers podem receber sitemap obsoleto/de outro host e robots sem
`sitemap`. O `sw.js` já trata `/sitemap.xml` como network-only.

### 13.3 Patch recomendado

- Manter os rewrites **antes** do `-f` (como está).
- Remover `public/sitemap.xml` e `public/robots.txt` do repositório (com aval) ou
  substituí-los por arquivos neutros.
- Testar:
  ```bash
  curl -s -D - https://SEU_HOST/sitemap.xml | head -20
  # 200, Content-Type: application/xml, <loc> do host atual
  curl -s -D - https://SEU_HOST/robots.txt
  # 200, text/plain, Sitemap: https://SEU_HOST/sitemap.xml
  ```
- Validar o XML no validador de sitemap do Google.

---

## 14. F-13 — `CacheControl`/`CspNonce` em respostas não-HTML

### 14.1 Evidência

`CacheControl` filtra por controller `Portal\`, método GET, status < 400, mas
**não** por `Content-Type`. Assim, ele também define `Cache-Control: public` e
`ETag` em `/robots.txt`, `/sitemap.xml` e `/manifest.webmanifest` (todos
controllers `Portal\`).

`CspNonce::shouldApply` também só verifica GET/segmento; ele emite o header CSP
em respostas não-HTML (o corpo não é mutado porque `injectNonceIntoHtml` checa
`text/html`).

### 14.2 Impacto

- `manifest.webmanifest` tem cor de marca dinâmica; cache público de 300s atrasa
  mudanças de tema.
- `sitemap.xml`/`robots.txt` com cache público de 300s é aceitável, mas deve ser
  intencional.
- Header CSP em `text/plain`/XML é inócuo, mas ruído.

### 14.3 Patch recomendado

Em `CacheControl`, exigir `text/html` para o caminho de cache público (e deixar
XML/JSON com `no-cache` ou TTL próprio):

```php
$contentType = (string) $response->headers->get('Content-Type', '');
if (strpos($contentType, 'text/html') === false) {
    return $response;
}
```

Em `CspNonce`, fazer o mesmo antes de emitir o header.

---

## 15. F-14 — `HtmlMinify` e blocos aninhados

### 15.1 Evidência

O regex de preservação:

```php
'#<(' . implode('|', $this->preserveTags) . ')(\s[^>]*)?>.*?</\1>#is'
```

não é um parser; para tags aninhadas da mesma família (ex.: `<pre><pre>...</pre>
...</pre>` ou `<svg><svg>...</svg></svg>`), ele captura até o primeiro
fechamento e o restante pode ser minificado. Também pode casar `<script>` dentro
de comentários/strings.

### 15.2 Impacto

Baixo no HTML atual (poucos aninhamentos), mas é um risco real em conteúdo
editável pelo CMS (blocos de código, SVG inline).

### 15.3 Patch recomendado

- Se o conteúdo tiver blocos aninhados, usar um parser DOM ou uma pilha de
  abertura/fechamento em vez de regex.
- Adicionar testes com `<pre><pre>`, `<svg><svg>` e `<script>` contendo
  `</script>` em string.
- Enquanto não houver parser, manter a decisão de não remover comentários e
  preservar blocos sensíveis.

---

## 16. F-15 — Registry do FPC

### 16.1 Evidência

`PageCache::store()` faz:

```php
$registry[] = $key;
Cache::forever(self::REGISTRY_KEY, $registry);
```

Se a mesma URL sofre MISS várias vezes (após TTL), a chave é adicionada
novamente; o array cresce com duplicatas até `MAX_KEYS` (2000). `flush()` tenta
`forget` duplicatas (a segunda chamada retorna falso) e o `count` pode ser menor
que o número de entradas.

### 16.2 Impacto

Baixo, mas o registry pode ficar maior que o necessário e o `count` do comando
não reflete entradas únicas.

### 16.3 Patch recomendado

```php
$registry = Cache::get(self::REGISTRY_KEY, []);
if (!in_array($key, $registry, true)) {
    $registry[] = $key;
}
```

E usar `array_unique` no flush.

---

## 17. F-16 — Blocklist H8 incompleta

### 17.1 Evidência

O `[H8]` bloqueia `.env`, `.git`, `.user.ini`, `composer.*`, `package*.json`,
`phpunit.xml`, `web.config`, `error_log`. Não bloqueia:

- `.htaccess` / `.htpasswd` (normalmente protegidos pelo Apache, mas defesa em
  profundidade);
- `artisan` (não fica em `public/`, mas se o docroot for mal configurado…);
- `storage/logs/laravel.log` / `csp-violations.log` (fora de `public/`, mas…);
- `vendor/` (fora de `public/`);
- `.gitlab-ci.yml`, `phpunit.xml.dist`, `composer.phar`.

### 17.2 Patch recomendado

Adicionar condições curtas ao bloco `mod_rewrite` (mesmo padrão de
`THE_REQUEST`), por exemplo:

```apache
RewriteCond %{THE_REQUEST} \s/\.htaccess[/?\s] [NC,OR]
RewriteCond %{THE_REQUEST} \s/\.htpasswd[/?\s] [NC,OR]
RewriteCond %{THE_REQUEST} \s/artisan[/?\s] [NC,OR]
RewriteCond %{THE_REQUEST} \s/\.gitlab-ci\.yml[/?\s] [NC,OR]
RewriteCond %{THE_REQUEST} \s/composer\.phar[/?\s] [NC]
```

> Não remova os itens existentes; apenas acrescente. Teste cada path com
> `curl -o /dev/null -w '%{http_code}'`.

---

## 18. F-17 — CI/CD não executa a cadeia de produção

### 18.1 Evidência

O `.gitlab-ci.yml` (jobs `deploy_homolog`/`deploy_production`) executa, via SSH:

```text
composer install --no-scripts
composer dump-autoload --no-scripts -q
php artisan package:discover
php artisan storage:link
php artisan view:clear
php artisan migrate --force
php artisan db:seed --class=ExtrasSeeder
composer dump-autoload --no-scripts -q
```

Não há `composer prod`, `config:cache`, `route:cache` nem restart de FPM.

### 18.2 Impacto

Um deploy feito **somente** pelo CI pode ficar sem os caches de bootstrap e sem
o reset de OPcache (`validate_timestamps=0`), servindo código/config antigos.
O `package:discover` manual cobre parte do `post-autoload-dump`, mas o
`dump-autoload` final não é `--optimize`/`--classmap-authoritative`.

### 18.3 Patch recomendado

No job de produção, após o `migrate`:

```bash
composer prod
# e, no host (root/WHM):
whmapi1 restart_service service=php-fpm
```

Se o pipeline não tiver permissão de root, separar o reset em um step
operacional e **falhar** o deploy se ele não for executado. Alternativa:
um endpoint autenticado de `opcache_reset()` no SAPI FPM (ver F-06).

Também vale rodar `composer validate --strict` e usar `composer install` com
lock (F-04).

---

## 19. Notas finais

- **Nada deste capítulo foi aplicado.** O diretório `infra/` é documentação.
- Antes de qualquer patch: backup, canário, `php -l`, suíte, `composer validate`.
- Em produção, `opcache.validate_timestamps=0`: **todo** patch PHP exige
  restart/reload do FPM.
- Se houver conflito entre este documento e o host real, vencem o `phpinfo`, os
  headers HTTP e os logs.
