# Segurança

Este documento descreve o que foi implementado, por quê, e o que **você** ainda
precisa fazer no ambiente. Segurança de aplicação não substitui segurança de
infraestrutura.

---

## 1. Modelo de ameaças

Quem realisticamente ataca um sistema de digital signage:

| Ameaça | Impacto | Defesa principal |
|---|---|---|
| Operador de um cliente acessa dados de outro | Vazamento entre concorrentes; quebra de contrato | Escopo de `tenant_id` na camada de modelo |
| Invasor exibe conteúdo próprio nas telas | Dano reputacional grave e público | Auth forte, RBAC, auditoria, token por dispositivo |
| Upload de arquivo executável | Execução remota de código | Uploads fora do docroot + validação de conteúdo |
| Roubo das credenciais das câmeras | Acesso ao circuito de segurança do cliente | AES-256-GCM em repouso, nunca exposto na interface |
| Força bruta na tela de login | Comprometimento de conta | Rate limit persistido + bloqueio progressivo |
| Força bruta no código de pareamento | Sequestro de uma TV | Rate limit por IP + expiração do código |
| XSS armazenado via nome de conteúdo | Sessão do administrador roubada | Escape em toda saída + CSP com nonce |
| Interceptação das URLs de mídia | Acesso indevido ao conteúdo | URLs assinadas com HMAC e validade curta |

---

## 2. Autenticação

**Senhas.** Argon2id (64 MB, 4 iterações, 2 threads); bcrypt custo 12 quando
Argon2id não está disponível. Rehash automático quando o algoritmo padrão muda.

**Política.** Mínimo de 12 caracteres, bloqueio de sequências comuns e do próprio
e-mail. Segue o NIST SP 800-63B: comprimento pesa mais que regras decorativas de
símbolos — que na prática produzem `Senha@2024` e um post-it no monitor.

**Rate limit.** Persistido em `login_attempts` — não em sessão, que o atacante
descarta a cada tentativa. Limite por e-mail (5 em 15 min) **e** por IP (20 em
15 min); o limite de IP é mais folgado porque escritórios compartilham NAT.

**Enumeração de contas.** Mensagem idêntica para e-mail inexistente e senha
errada. No caminho do e-mail inexistente há um `password_verify` contra hash
falso, para igualar o tempo de resposta.

**Sessões.** Em banco (`user_sessions`), o que permite revogação imediata —
"encerrar todas as sessões" tem efeito instantâneo, sem esperar cookie expirar.
Cookie `HttpOnly` + `SameSite=Lax` + `Secure`; ID regenerado no login e a cada
15 minutos; expiração por inatividade (2 h) e absoluta (12 h); fingerprint do
User-Agent.

---

## 3. Autorização e isolamento entre clientes

**Este é o controle mais importante do sistema.**

O isolamento não depende de o controller lembrar de filtrar. `Models\Model`
injeta `tenant_id = ?` em toda leitura e escrita:

```php
protected static function scope(string $alias = ''): array
{
    // ... tenant_id sempre presente; sem tenant e sem ser superadmin => 1 = 0
}
```

Consequências práticas:

- `Model::find(999)` devolve `null` se o registro for de outro cliente
- `Model::update()` e `delete()` não alcançam registros fora do escopo
- `Model::create()` **ignora** qualquer `tenant_id` vindo do formulário

Além disso, relacionamentos são validados: adicionar mídia a uma playlist,
definir alvos de agendamento ou membros de grupo verificam se o objeto
referenciado pertence ao mesmo cliente. Sem isso, um `<input>` adulterado
colocaria conteúdo na TV de outra empresa.

**RBAC** em `Auth::PERMISSIONS`, verificado nas rotas (`'can' => 'media.manage'`)
e reforçado nos controllers. `x.manage` implica `x.view`.

---

## 4. Injeção de SQL

Prepared statements com `ATTR_EMULATE_PREPARES => false` — o servidor separa
query de dados, fechando também o vetor de injeção por charset multibyte.

Onde SQL precisa de identificadores dinâmicos (ORDER BY, filtros), o valor passa
por whitelist:

```php
if (!in_array($sort, static::$sortable, true)) { $sort = 'id'; }
$dir = strtoupper($dir) === 'ASC' ? 'ASC' : 'DESC';
```

Nomes de coluna vindos de definição de campo são validados com
`preg_match('/^[a-z_]+$/', $col)` antes de qualquer interpolação.

---

## 5. XSS

Toda saída em view passa por `e()` (`htmlspecialchars` com `ENT_QUOTES`).
Para dado embutido em JavaScript, `ejs()` aplica `json_encode` com flags `HEX_*`.

**CSP com nonce**, sem `'unsafe-inline'` em `script-src`. Cada requisição gera um
nonce; scripts inline do painel o carregam. Um `<script>` injetado no banco não
executa — não tem o nonce da requisição.

SVG **não** é aceito no upload: é XML com JavaScript executável.

---

## 6. CSRF

Token sincronizador por sessão, validado em todo POST/PUT/DELETE do painel,
via campo `_token` ou header `X-CSRF-Token`. Comparação com `hash_equals`.
Rotacionado no login e no logout.

A API dos dispositivos (`/api/v1/*`) **não** valida CSRF — por design. Ela não
usa cookie de sessão; autentica por Bearer token, então não é alvo desse ataque.

---

## 7. Uploads

Cinco camadas, porque nenhuma isolada é suficiente:

1. **Fora do docroot** (`storage/uploads`), servidos por controller autenticado
2. **Nome gerado por nós** — `20260815-143022-a3f9c1.mp4`. Elimina path traversal, `.php`, `.htaccess`
3. **Extensão em whitelist E MIME real** via `finfo` — o `Content-Type` do browser é ignorado
4. **Imagem revalidada** por `getimagesize()`; recusa acima de 100 MP (decompression bomb)
5. **Permissão 0640** + `.htaccess` com `php_flag engine off` no diretório

`Uploader::absolutePath()` faz verificação de contenção com `realpath()`: mesmo
que um caminho malicioso entre no banco, ele não escapa da raiz de uploads.

---

## 8. Criptografia

**AES-256-GCM** (autenticada) para credenciais RTSP e segredos TOTP em repouso.
GCM e não CBC: ciphertext adulterado falha na verificação da tag em vez de
decifrar lixo silenciosamente.

**URLs assinadas** para as TVs. A TV não tem sessão; a autorização viaja na URL:

```
/m/42?d=7&e=1755302400&s=<hmac-sha256>
```

O HMAC cobre `media_id`, `device_id` e expiração. Validade padrão de 6 horas.
O controller ainda confirma que a mídia e o dispositivo pertencem ao mesmo cliente.

**Tokens de dispositivo:** 32 bytes aleatórios, armazenados como SHA-256.
Vazamento do banco não entrega tokens utilizáveis.

---

## 9. SSRF

Câmeras RTSP são um vetor clássico: o usuário aponta a "câmera" para um endereço
interno e usa o relay do servidor como sonda de rede.

`Stream::validateRtspUrl()` bloqueia loopback (`127.x`, `::1`), `0.x` e
link-local (`169.254.x` — inclui o endpoint de metadados de nuvem em `169.254.169.254`).
Redes privadas (`10.x`, `192.168.x`) **são** permitidas: a câmera legítima está
na LAN do cliente.

URLs de página web e YouTube exigem `http(s)://` — `javascript:`, `data:` e
`file:` são rejeitados na validação.

---

## 10. Cabeçalhos HTTP

```
Content-Security-Policy      (com nonce, sem unsafe-inline em script-src)
Strict-Transport-Security    max-age=31536000; includeSubDomains
X-Frame-Options              DENY (painel) / SAMEORIGIN (player)
X-Content-Type-Options       nosniff
Referrer-Policy              strict-origin-when-cross-origin
Permissions-Policy           geolocation=(), microphone=(), camera=(), payment=()
Cross-Origin-Opener-Policy   same-origin
```

`X-Powered-By` removido.

---

## 11. Auditoria

`audit_log` registra ação, entidade, valores antes/depois, IP e User-Agent.
Campos sensíveis (`password_hash`, `token_hash`, `*_enc`) são substituídos por
`***` antes de gravar. O e-mail do usuário é desnormalizado — a trilha sobrevive
à exclusão da conta.

Tentativas negadas por permissão são registradas como `denied`: é o sinal mais
confiável de conta comprometida ou de operador testando limites.

---

## 12. Checklist de produção

Antes de colocar no ar:

- [ ] `APP_DEBUG=false` no `.env`
- [ ] `COOKIE_SECURE=true` e TLS válido instalado
- [ ] `APP_KEY` e `URL_KEY` geradas (`php bin/keygen.php`), **backup em cofre**
- [ ] `.env` com permissão `0640`, dono do usuário do servidor web
- [ ] Docroot apontando para `public/`, não para a raiz
- [ ] `storage/` fora do docroot e inacessível por URL — teste: `curl https://seu.site/storage/`
- [ ] Usuário MySQL sem `SUPER`, `FILE` ou `GRANT` — só DML/DDL no schema da aplicação
- [ ] `expose_php=Off` no php.ini
- [ ] Cron de manutenção ativo
- [ ] Backup automatizado do banco **e** de `storage/uploads`
- [ ] Superadministrador com senha própria (não a provisória do instalador)
- [ ] `TRUSTED_PROXIES` configurado se houver CDN/load balancer à frente

### Teste manualmente após o deploy

```bash
# Nenhum destes deve devolver conteúdo:
curl -I https://seu.site/.env
curl -I https://seu.site/storage/uploads/
curl -I https://seu.site/src/Core/Auth.php
curl -I https://seu.site/database/schema.sql

# Deve redirecionar para HTTPS:
curl -I http://seu.site/login

# Deve devolver 401:
curl -I https://seu.site/api/v1/playlist
```

Teste também o isolamento entre clientes: entre como admin do cliente A e tente
abrir `/devices/{id}` de uma TV do cliente B. Deve dar 404, não 403 — não
confirmamos sequer que o registro existe.

---

## 13. O que **não** está implementado

Transparência sobre limites conhecidos:

- **2FA/TOTP** — o schema tem as colunas (`totp_secret`, `totp_enabled`), mas o
  fluxo de verificação não foi construído. Recomendado antes de escalar
- **Recuperação de senha por e-mail** — a tabela `password_resets` existe; o
  envio não. Hoje a redefinição é feita por um administrador
- **Assinatura de conteúdo** — o player confia no que o servidor entrega. Contra
  um servidor comprometido, não há defesa no cliente
- **WAF / proteção DDoS** — camada de infraestrutura (Cloudflare, ModSecurity)
- **Criptografia em repouso do banco** — use a do SGBD ou do volume

---

## 14. Resposta a incidente

Suspeita de conta comprometida:

1. `/users/{id}/edit` → **Encerrar todas as sessões** (efeito imediato)
2. Trocar a senha marcando "senha provisória"
3. `/reports/audit` filtrando pelo usuário — verifique ações `delete`, `denied` e `command`
4. Se uma TV exibiu conteúdo indevido: `/devices/{id}` → **Reparear**, o que invalida o token

Suspeita de vazamento das chaves (`APP_KEY`/`URL_KEY`):

1. Gere novas com `php bin/keygen.php`
2. Recadastre as credenciais de todas as câmeras (a `APP_KEY` antiga as cifrava)
3. As TVs se recuperam sozinhas — as URLs em cache falham e são renovadas no ciclo seguinte
