Cabeçalhos de segurança explicados: CSP, HSTS e os demais
Um guia prático de CSP com nonces e strict-dynamic, HSTS e preload, frame-ancestors, Referrer-Policy, Permissions-Policy e COOP, com uma configuração Next.js.
· 7 min de leitura · Lina Source LLC
Cabeçalhos de segurança são instruções que seu servidor dá ao navegador: execute apenas estes scripts, fale comigo só por HTTPS, não deixe outros sites colocarem esta página num frame. Eles não corrigem falhas no seu código, mas limitam o que um atacante consegue fazer com uma delas. Um buraco de cross-site scripting atrás de uma Content Security Policy estrita é um problema muito menor do que o mesmo buraco sem ela.
A maioria desses cabeçalhos é uma linha de configuração. A exceção é a CSP, que exige planejamento. Este guia cobre o que cada cabeçalho faz, valores sensatos para uma aplicação web típica e como implantá-los sem quebrar a produção.
Content-Security-Policy
A CSP diz ao navegador quais origens de scripts, estilos, imagens, frames e conexões são permitidas. Seu papel principal é impedir que scripts injetados rodem. Uma allowlist de domínios parece a abordagem óbvia, mas tem um longo histórico de burlas: qualquer CDN permitida que hospede conteúdo de usuários ou versões antigas de bibliotecas pode ser usada para carregar script controlado pelo atacante.
Nonces e strict-dynamic
A abordagem que se sustenta é uma política baseada em nonce. O servidor gera um valor aleatório por resposta, coloca-o no cabeçalho CSP e o adiciona como atributo nonce em cada tag de script que renderiza. Tags de script injetadas não conhecem o nonce e são bloqueadas. Acrescentar 'strict-dynamic' permite que scripts carregados por um script confiável carreguem outros scripts, o que mantém bundlers e gerenciadores de tags funcionando sem listar cada domínio.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-4AEemGb0xJptoIGFP3Nd' 'strict-dynamic';
style-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd';
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsO cabeçalho é enviado em uma única linha; aqui ele está quebrado para facilitar a leitura. Algumas diretivas dessa política fazem mais do que aparentam. object-src 'none' bloqueia conteúdo de plugins legados. base-uri 'none' impede que uma tag base injetada redirecione URLs relativas de scripts. form-action 'self' impede que formulários injetados enviem credenciais para outro lugar. O nonce precisa ser imprevisível e novo a cada resposta, o que significa que páginas que o usam não podem ser servidas de um cache estático.
Implante com Report-Only
Uma CSP estrita implantada às cegas vai quebrar alguma coisa: um trecho de analytics, um handler de evento inline, um widget de terceiros. Publique a política primeiro como Content-Security-Policy-Report-Only. O navegador não aplica nada, mas reporta toda violação ao endpoint indicado na diretiva report-to ou report-uri. Colete relatórios por um tempo, corrija ou libere o que for legítimo e então troque o nome do cabeçalho para passar a aplicar. Mantenha o reporte ligado depois de aplicar, porque violações novas são regressões ou ataques. Espere ruído nos relatórios vindo de extensões de navegador, que injetam seus próprios scripts nas páginas; filtre-as pela origem antes de decidir o que liberar.
Strict-Transport-Security
O HSTS diz ao navegador para usar HTTPS no seu domínio por um período determinado, mesmo que a pessoa digite http:// ou clique num link antigo. Isso fecha a janela em que um atacante na rede poderia interceptar a primeira requisição HTTP em texto claro e remover o redirecionamento para HTTPS. Os navegadores só honram o cabeçalho quando ele chega por HTTPS, e também se recusam a deixar as pessoas ignorarem erros de certificado num domínio com HSTS, o que é justamente o objetivo mas também o risco se um certificado vencer.
Comece com um max-age curto, como um dia, confirme que nada quebrou e então aumente para um ou dois anos. O includeSubDomains estende a regra a todo subdomínio, então verifique antes que todos eles sirvam HTTPS válido, incluindo sites antigos de marketing e ferramentas internas no mesmo domínio.
A diretiva preload, combinada com um envio à lista de preload dos navegadores, grava seu domínio dentro dos navegadores para que até a primeiríssima visita use HTTPS. Ela exige um max-age de pelo menos um ano e includeSubDomains. Trate isso como uma porta de mão única: a remoção da lista é possível, mas leva muito tempo para chegar às pessoas, e qualquer subdomínio que não consiga fazer HTTPS fica inacessível nesse meio-tempo.
Os cabeçalhos de uma linha
- X-Content-Type-Options: nosniff. Impede que navegadores adivinhem um tipo de conteúdo diferente do que você declarou, o que evita que um arquivo enviado e servido como texto seja executado como script.
- frame-ancestors (na CSP) e X-Frame-Options: DENY. Controlam quem pode embutir suas páginas num frame, que é a defesa contra clickjacking. Navegadores modernos usam frame-ancestors e ignoram X-Frame-Options quando ambos estão definidos; mantenha X-Frame-Options para clientes antigos. Use 'self' ou SAMEORIGIN se você embute suas próprias páginas.
- Referrer-Policy: strict-origin-when-cross-origin. Envia apenas a sua origem, e não o caminho completo e a query string, para outros sites. Isso evita que tokens e IDs em URLs vazem para terceiros. Use no-referrer em páginas especialmente sensíveis.
- Permissions-Policy. Desliga recursos do navegador que você não usa, como camera=(), microphone=(), geolocation=() e payment=(), para que código injetado ou embutido não consiga solicitá-los.
- Cross-Origin-Opener-Policy: same-origin. Coloca sua página no próprio grupo de contexto de navegação, para que uma janela aberta por outro site não consiga manter uma referência a ela. Use same-origin-allow-popups se você depende de popups de OAuth ou de pagamento.
- Cross-Origin-Resource-Policy: same-origin ou same-site. Diz aos navegadores para não deixarem outras origens carregarem suas respostas como imagens, scripts ou outros sub-recursos. Use same-site se você serve assets de um subdomínio irmão, e cross-origin para assets feitos para serem embutidos em outros lugares.
Você pode descartar o X-XSS-Protection. O filtro que ele controlava foi removido dos navegadores modernos, e a CSP é o substituto. Se algum scanner insistir, defina-o como 0. Da mesma forma, evite cabeçalhos que só dão informação ao atacante: X-Powered-By e banners de Server detalhados revelam seu framework e sua versão de graça. No Next.js, defina poweredByHeader: false na configuração para remover o primeiro.
Uma configuração Next.js
Cabeçalhos estáticos pertencem ao next.config.ts, onde a função headers() os aplica a todas as rotas. Os valores abaixo são um padrão sensato para uma aplicação que não se embute em frames, não usa câmera nem localização e serve os próprios assets. Ajuste cada um ao que sua aplicação realmente faz, em vez de copiá-los às cegas.
// next.config.ts
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=()",
},
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Resource-Policy", value: "same-origin" },
];
const nextConfig: NextConfig = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;A CSP precisa de um nonce novo por requisição, então ela é definida no middleware. O Next.js lê o nonce do cabeçalho Content-Security-Policy da requisição e o aplica aos scripts do próprio framework durante a renderização. Você pode lê-lo num server component com headers().get("x-nonce") para suas próprias tags de script.
// middleware.ts (no Next.js 16 o arquivo é proxy.ts e a função é proxy)
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' blob: data:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests",
].join("; ");
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};Páginas renderizadas com um nonce precisam ser renderizadas dinamicamente, já que uma página estática reutilizaria o mesmo nonce para todos os visitantes. Durante a implantação, troque o nome do cabeçalho nos dois lugares para Content-Security-Policy-Report-Only e adicione um endpoint de reporte. Em desenvolvimento, o React pode precisar de 'unsafe-eval' no script-src para seus recursos de depuração; acrescente isso apenas quando NODE_ENV for development.
Como verificar
- Rode curl -sI https://seu-dominio.example/ contra a página inicial, uma página dinâmica, uma rota de API e um asset estático, e leia os cabeçalhos em cada uma. Cabeçalhos definidos só em páginas HTML são uma lacuna comum.
- Abra as ferramentas de desenvolvedor do navegador. Violações de CSP aparecem no console com a diretiva e o recurso bloqueado, e a aba Network mostra os cabeçalhos que foram realmente servidos.
- Verifique atrás da sua CDN ou proxy reverso, além de na origem. Proxies às vezes removem, duplicam ou sobrescrevem cabeçalhos, e dois cabeçalhos CSP conflitantes são ambos aplicados, então a política efetiva fica mais estrita que qualquer uma delas.
- Use um verificador público, como o Mozilla HTTP Observatory, para uma segunda opinião rápida, e o CSP Evaluator do Google para identificar diretivas CSP fracas.
- Adicione um teste no CI que requisite as rotas principais e verifique que os cabeçalhos estão presentes, para que uma refatoração de configuração não consiga removê-los silenciosamente.
Cabeçalhos são configuração, e é exatamente por isso que eles se desviam: um route handler novo que monta a própria resposta, uma regra de proxy que sobrescreve os padrões, uma CSP afrouxada com 'unsafe-inline' para destravar uma entrega. Revise-os como código. O CodeAuditAgent lê a configuração de um repositório público ou de um trecho colado junto com o resto do código, e reporta cabeçalhos ausentes ou enfraquecidos com severidade, a configuração citada e uma correção sugerida.
Uma ordem de trabalho razoável: os cabeçalhos de uma linha hoje, o HSTS com um max-age curto esta semana, e uma CSP baseada em nonce em modo Report-Only assim que você conseguir coletar os relatórios.