A maioria das políticas permite mais do que o autor imagina.
Cole uma Content-Security-Policy e veja o que um navegador de fato vai deixar passar. Nada é enviado para nós: a política é interpretada no seu navegador, então dá para checar um header que você ainda nem publicou.
O que isso verifica, e como é uma boa resposta
Qual diretiva realmente governa seus scripts
Navegadores usam script-src se ela existir e recorrem a default-src se não existir. Esse fallback é fonte comum de surpresa: uma política com um default-src rígido e um script-src permissivo é exatamente tão permissiva quanto o segundo, e a linha rígida acima não faz nada pelos scripts. Mostramos qual diretiva lemos, para o veredito ser conferível em vez de algo que você tem que aceitar na fé.
unsafe-eval
Permite transformar strings em código executável, que é do que a maioria dos payloads de injeção depende. Costuma estar ali porque um ajuste de bundler ou uma biblioteca de template pediu, e costuma ser removível mudando esse ajuste. Enquanto estiver ali, boa parte do resto da política é decoração.
Curingas, e quando eles não contam
Um * solto, ou uma origem de esquema como https:, permite script de qualquer host da internet. Mas um curinga de host ao lado de 'strict-dynamic' não é um furo, porque essa palavra-chave manda o navegador ignorar origens de host por completo e confiar só no que um script aprovado por nonce carregar. Verificamos tokens inteiros em vez de procurar no texto, então https://cdn.exemplo.com/* é corretamente lido como um host específico e não como um curinga só porque contém um asterisco.
unsafe-inline, e quando ele já é ignorado
O motivo mais comum de uma política aparentemente rígida não se sustentar. Ela permite scripts inline, então markup injetado executa. A nuance que derruba a maioria dos verificadores: se a mesma diretiva carrega um nonce ou um hash, os navegadores ignoram 'unsafe-inline' por completo. As políticas são escritas assim de propósito para que navegadores antigos, que não entendem nonce, ainda carreguem a página. Sinalizar esse formato seria um falso positivo na política rígida correta mais comum que existe, então reportamos como tudo certo e dizemos por quê.
base-uri, a que quase sempre falta
base-uri não é coberta por default-src, então uma política pode parecer completa e ainda assim omiti-la. Sem ela, uma tag <base> injetada reaponta todo script e link relativo da página para outro host, o que transforma uma pequena injeção de HTML em execução completa de script e passa por cima de um script-src no qual você trabalhou bastante. Custa uma diretiva.
object-src e frame-ancestors
object-src 'none' fecha conteúdo de plugin, de que quase nenhuma aplicação precisa e que de outra forma pode rodar um documento na sua origem. frame-ancestors decide quem pode embutir sua página; é a substituta moderna de X-Frame-Options e a única das duas capaz de expressar uma lista de origens permitidas.
Como corrigir
Uma política rígida é mais fácil de alcançar do que a fama sugere, desde que você a publique primeiro em modo report-only e leia o que quebra. A abordagem de nonce abaixo é a que este site usa, então é um padrão que rodamos em produção e não apenas algo que recomendamos.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
# 'strict-dynamic' lets a nonce-approved script load what it needs
# without you listing every CDN, which is what makes a strict policy
# survivable in a real app with a bundler.# Same policy, different header name. Browsers report violations
# to the console and to report-to, and block NOTHING. Run it for a
# week, read what shows up, then rename the header.
Content-Security-Policy-Report-Only:
default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'// The nonce must be new on EVERY request. A nonce baked into a
// static file is not a nonce, it is a password the attacker also has.
const nonce = crypto.randomUUID().replaceAll("-", "");
const policy = [
"default-src 'self'",
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join("; ");
// Then stamp the same nonce onto every <script> as you stream the HTML.
return new HTMLRewriter()
.on("script", { element: (el) => el.setAttribute("nonce", nonce) })
.transform(new Response(html, { headers: { "content-security-policy": policy } }));// middleware.ts
import { NextResponse } from "next/server";
export function middleware(request: Request) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const policy = `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self'`;
const headers = new Headers(request.headers);
headers.set("x-nonce", nonce);
const response = NextResponse.next({ request: { headers } });
response.headers.set("content-security-policy", policy);
return response;
}Isto lê a política que você colou. Não consegue dizer se é a política que seu servidor realmente envia, e as duas discordam com mais frequência do que se imagina: um proxy, uma regra de CDN ou um segundo header no nível do framework podem sobrescrever ou duplicar, e quando duas políticas chegam o navegador aplica a interseção das duas. Também não consegue dizer se a política quebra seu app, coisa que só uma publicação em report-only mostra. Para ver o que seu site no ar envia, rode o verificador de headers de segurança contra a URL.