# 12 — Segurança e dados Duas partes. A primeira é o que foi encontrado e corrigido revisando o protótipo — vale porque o protótipo é a referência que vai ser copiada. A segunda é o que o servidor precisa fazer, e é onde a segurança de verdade se decide. --- ## O que a revisão encontrou Quatro passes: varredura funcional, consistência, injeção e permissão. Método e resultado abaixo, para poder ser repetido depois de cada mudança grande. > **Repetida depois de M19**, com os quatro papéis: 225 combinações de papel × fase × tela sem > erro; 208 com todo campo envenenado, zero injeção; 7 famílias de permissão, zero falha. ### Injeção — 8 pontos onde dado digitado entrava cru no HTML Método: envenenar **todo** campo que uma pessoa digita com três cargas (`">`, `` e quebra de aspas simples), percorrer 120 combinações de modo × fase × tela, e contar tags perigosas aparecendo no DOM. | onde | o dado vem de | consequência | |---|---|---| | `ESTUDIO.sigla` no avatar e no logo do cliente | nome do estúdio | toda tela, inclusive a do cliente | | `i.num` na etiqueta do cartão, no quadro e no relatório | **nome do arquivo enviado** | a entrada menos confiável do produto | | `AMBS[i.amb]` no cartão, no visualizador e no relatório | campo de ambiente | idem | | `e.nome` no `title` do cabeçalho do quadro | nome da etapa, editável | quebra de atributo | | `statusPublico()` no acompanhamento | rótulo público da etapa | **renderizado na página do cliente** | | `a.num`, `a.mpx`, `a.mb` no upload | nome do arquivo | idem | | `l.sub` no cronograma | rótulos de etapa | latente | | número do item na lista do relatório | nome do arquivo | idem | O mais grave é o padrão, não o caso: **o nome do arquivo é a entrada menos confiável que existe aqui** — vem de fora, passa por três telas e acabava dentro de atributo HTML. Depois da correção: 120 combinações, todo campo envenenado, **zero tags injetadas e zero execuções**. ### Permissão — 2 furos na regra que define o produto **O relatório não obedecia à fase.** A equipe abria a aba Relatório durante a janela do cliente e lia os comentários dele inteirinhos. A regra 1 estava implementada na grade, no visualizador e na fila — e furada pela porta dos fundos, que é como esse tipo de regra costuma vazar: numa tela escrita antes da regra existir e nunca revisitada. **O estúdio conseguia editar comentário do cliente.** `docs/09-melhorias.md` diz "nunca", e o código dizia o contrário porque a checagem aceitava o modo estúdio agindo em nome do cliente. Registro que a outra parte pode reescrever não vale como prova numa discussão de escopo — que é metade do valor do produto. ### Robustez — 9 ações que estouravam fora de contexto Nenhuma alcançável pela interface, todas alcançáveis se o estado sair de sincronia. Guardadas. ### Reteste | verificação | resultado | |---|---| | 165 combinações modo × fase × tela | sem erro, sem `undefined`, sem vazamento horizontal | | 120 combinações com todo campo envenenado | zero injeção | | 6 famílias de regra de permissão | zero falha | | toda ação disparada fora de contexto | zero exceção | ### O que apareceu ao rodar o backend contra MySQL de verdade Dois defeitos que nenhuma leitura de código pegaria, e nenhum banco de mentira também: **`CHECK` e `ON DELETE SET NULL` na mesma coluna.** O MySQL 8 recusa criar a tabela: *"Column 'revisor_id' cannot be used in a check constraint: needed in a foreign key referential action."* E ele está certo — a ação da chave poderia apagar o autor e violar o próprio `CHECK`. A correção certa não foi afrouxar o `CHECK`. `docs/11` já dizia que quem escreve **não se apaga, se desativa** (`usuarios.ativo`, `revisores.ativo`), e `SET NULL` contradizia isso: deixaria o comentário órfão de autor, que é o oposto de auditoria. As chaves de autoria em `comentarios` e `comentario_respostas` passaram a `ON DELETE RESTRICT` — agora o banco impõe a intenção que a documentação já declarava. **Duas implementações da mesma regra.** O migrador e o corredor de testes liam o schema cada um do seu jeito. O do migrador descartava o trecho inteiro quando ele começava com linha de comentário — e engolia o `CREATE TABLE estudios`, que vem logo abaixo de um. O teste passava porque exercitava o outro caminho. Virou `Aprova\Schema`, usado pelos dois: teste que não percorre o mesmo código do produto não testa o produto. --- ## O que o servidor precisa fazer O protótipo não tem servidor. Tudo abaixo é obrigação de quem implementar, e nada disso é substituído por esconder botão na tela. ### Papéis, depois de M19 Quatro, e cada um precisa de teste próprio: | papel | entra por | escopo | |---|---|---| | estúdio · admin | senha | o estúdio inteiro, cobrança, equipe | | estúdio · artista | senha | produção; **não** vê comentário antes da liberação | | cliente · gerente | senha | os projetos do **cliente dele**, e a lista de revisores | | cliente · revisor | link mágico | os projetos em que foi incluído | O gerente é o papel novo e o mais fácil de errar: ele tem senha, então parece usuário do estúdio, e não é. **Sessão de revisor nunca pode alcançar rota do estúdio**, e um gerente do cliente A nunca alcança projeto do cliente B — mesmo dentro do mesmo estúdio. O filtro passa a ser duplo: `estudio_id` da sessão **e** `cliente_id` quando o dono da sessão é do lado do cliente. Teste obrigatório, além dos de inquilino: autenticado como gerente do cliente A, pedir por id direto um projeto, comentário, tarefa e etapa do cliente B — e esperar 404. ### O link mágico do revisor - só envia se o endereço for revisor daquele projeto, e a resposta na tela é a mesma nos dois casos — senão o campo vira sonda para descobrir quem revisa o quê - limite por endereço e por IP, senão o formulário vira ferramenta de spam com o seu domínio no remetente - uso único, 30 minutos, e o que ele abre é uma sessão de 30 dias — o token não é o acesso, é a porta - remover a pessoa mata a sessão dela na hora ### A regra que não pode falhar **Fase da rodada é autorização, não enfeite de interface.** A tabela de `docs/04-modelo-de-dados.md` precisa valer em toda rota, no servidor: | fase | cliente escreve | equipe lê comentário | equipe escreve | |---|---|---|---| | rascunho | não | não | não | | cliente | **sim** | **não** | não | | fechada | não | não | não | | equipe | não | sim | **sim** | | concluida | não | sim | não | E as duas que a revisão acrescentou: - **tarefa interna nunca sai numa resposta ao token do cliente** — nem na lista, nem na contagem por item, nem no acompanhamento. O filtro fica na camada de acesso a dados, num ponto só, não repetido em cada rota - **etapa com `publica = 0` nunca sai numa resposta ao token do cliente**, embora conte no percentual Teste automatizado para cada linha dessas tabelas. É o requisito de teste mais importante do produto, porque é a regra que diferencia o produto. ### Isolamento entre estúdios `estudio_id` vem **da sessão**, nunca da requisição. Centralizar o acesso a dados numa camada que injeta o filtro, para não depender de cada desenvolvedor lembrar. Teste obrigatório: autenticado no estúdio A, pedir por id direto um projeto, imagem, tile, comentário, anexo, tarefa, etapa e entrega do estúdio B — e esperar 404 em todos. **404, não 403**: 403 confirma que o recurso existe. ### Servir imagem Nada em `/storage/` acessível por URL. Todo byte sai por endpoint PHP que valida sessão ou token antes de responder — inclusive tile, miniatura, anexo, pôster e original. Caminho de arquivo montado a partir de id numérico do banco, **nunca de string da requisição**. É onde travessia de diretório entra. Anexo enviado pelo cliente é arquivo de estranho: validar o tipo pelo conteúdo e não pela extensão, reescrever a imagem (o que descarta payload em metadado), servir com `Content-Disposition: attachment` e `Content-Type` fixo, e nunca de um caminho que o servidor web execute. ### Upload Limite de tamanho e de quantidade por requisição; tipo verificado pelo conteúdo; nome de arquivo original guardado como dado e **nunca usado para montar caminho**. Quota por estúdio antes de aceitar o pedaço, senão qualquer conta enche o disco de todo mundo. ### O link aberto — quando o estúdio liga Desligado por padrão desde M19. Ligado, é a superfície mais exposta do produto. - 32 bytes de `random_bytes()`, hash no banco, `hash_equals()` na comparação - `` na página do cliente, senão o token vaza no `Referer` para todo domínio que ele clicar - `X-Robots-Tag: noindex, nofollow` — link vazado que entra em buscador é vazamento permanente - limite de requisição por token, senão o link vira porta de varredura - regenerar invalida o anterior na hora, e o estúdio precisa conseguir fazer isso sem suporte **Aceite explicitamente:** quem receber o link vê o projeto, e o comentário que sair dele não tem autor confiável. Por isso não é mais o caminho padrão — é a saída para o cliente que se recusa a abrir e-mail, e a tela avisa disso na hora de ligar. Tudo acima vale também para o link mágico do revisor, com uma diferença a favor: ele é de uso único e curto, então vazar o `Referer` de um link já usado não dá acesso a ninguém. ### Sessão, senha e token Detalhado em `docs/11-contas-e-emails.md`. Em resumo: cookie `HttpOnly` `Secure` `SameSite=Lax`, sessão em tabela para poder ser derrubada, `password_hash()`, token com hash no banco e uso único, limite de tentativa por conta **e** por IP, mensagem de erro que não diferencia e-mail inexistente de senha errada. **CSRF:** token por sessão em toda rota que altera dado, comparado com `hash_equals()`. `SameSite=Lax` ajuda e não basta. ### Cabeçalhos ``` Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; frame-ancestors 'none'; base-uri 'none' Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff Referrer-Policy: same-origin (no-referrer na página do cliente) ``` CSP sem `unsafe-inline` obriga a tirar o estilo e o script de dentro do HTML. O protótipo é autocontido de propósito; **o produto não pode ser**. --- ## Dados pessoais O que o produto guarda de pessoa física: nome e e-mail da equipe do estúdio, nome e e-mail do revisor do cliente, IP em `eventos` e em `tentativas_login`, e o texto que cada um escreveu. Pouco, e ainda assim é dado pessoal sob a LGPD. **O revisor não pediu para estar aqui.** Quem cadastrou o e-mail dele foi o estúdio. Na relação da LGPD o estúdio é o controlador e a plataforma é a operadora — o que precisa estar escrito no contrato de cada estúdio, não só nos termos de uso. O que decorre disso, e ainda não está construído: - **excluir um revisor** apagando nome e e-mail e mantendo os comentários com autor anônimo: o registro da rodada não pode sumir porque alguém saiu da empresa - **exportar o que há sobre uma pessoa**, para responder pedido de titular - **prazo de guarda do IP** — hoje `eventos` guarda para sempre, e não precisa **Retenção do arquivo** continua em aberto (P5 e P7 de `docs/07-pendencias.md`). Render de projeto não lançado é informação comercialmente sensível: quanto tempo o original fica no servidor, para onde vai depois, e o que acontece quando o estúdio cancela o plano. ### Neste repositório, agora Há renders reais de cliente e o nome do empreendimento. O repositório é privado, e `docs/08-privacidade.md` continua valendo: **trocar o conjunto antes de qualquer demonstração comercial ou ambiente acessível de fora.** Nada de senha, chave ou token real está versionado. `.gitignore` cobre `.env`, `config.local.php` e `/storage/`. Antes do primeiro deploy, conferir de novo — segredo que entra no histórico não sai só removendo o arquivo. --- ## Rodar a revisão de novo Não há suíte automatizada ainda. O que foi feito aqui é reproduzível no console do navegador, com o protótipo aberto, e o método está descrito em cada seção acima: envenenar todo campo digitável e contar tag perigosa no DOM; percorrer modo × fase × tela; disparar toda ação fora de contexto; e verificar cada linha da tabela de fases. Quando o backend existir, isso vira teste de verdade — e os casos de permissão são os primeiros a escrever, antes de qualquer tela.