Content Security Policy, diretiva por diretiva explica para que uma CSP serve e o que cada diretiva compra. Este artigo trata de algo mais estreito e, na prática, da causa da maioria das políticas que fazem menos do que os seus autores pensam: como o navegador lê o cabeçalho. O Level 3 (W3C Working Draft, 16 de setembro de 2026, lido em 2026-10-05) não deixa isso à intuição. Dá um algoritmo para transformar o texto do cabeçalho num objeto de política, uma gramática para cada token, uma cadeia de recurso para cada destino e regras de casamento para cada tipo de expressão de fonte. Cada passo tem um modo de falha silencioso: o navegador não rejeita uma política que não conseguiu entender, ele aplica a política que conseguiu entender. O avaliador de CSP faz a leitura abaixo e mostra o resultado; esta é a leitura.

Passo um: pontos e vírgulas, minúsculas, e a duplicata que desaparece

O algoritmo "Parse a serialized CSP" (2.2.1) começa por "strictly splitting serialized on the U+003B SEMICOLON character". Cada pedaço perde os espaços das pontas; pedaços vazios são pulados; o nome da diretiva é "the result of collecting a sequence of code points from token which are not whitespace", depois passado a minúsculas, de modo que "script-SRC 'none' and ScRiPt-sRc 'none' are equivalent". O resto do pedaço, dividido nos espaços, é o valor.

Então o passo que morde: "If policy's directive set contains a directive whose name is directive name, continue." Um segundo script-src mais adiante na mesma política não é mesclado nem sobrepõe; é descartado, e a especificação só pede que "the user agent SHOULD notify developers that a duplicate directive was ignored. A console warning might be appropriate". Uma política montada por duas camadas de configuração, cada uma acrescentando o seu script-src, fica com a que veio primeiro.

A regra do ponto e vírgula tem uma falha gêmea. Escreva script-src 'self' object-src 'none' com o ponto e vírgula esquecido e há uma diretiva, script-src, cujo valor tem quatro tokens. O terceiro, object-src, é lido contra a gramática de expressões de fonte e casa com host-source: é um servidor chamado object-src. A diretiva object-src que deveria fechar o sumidouro de plugins nunca existe.

Passo dois: cada token contra a gramática

Uma lista de fontes é "a whitespace-delimited series of source expressions", cada uma de cinco formas (2.3.1): uma scheme-source como https:, uma host-source como *.example.com ou https://example.com:12/path/to/file.js, uma keyword-source como 'self' ou 'unsafe-inline', uma nonce-source como 'nonce-ch4hvvbHDpv7xCSvXCs3BrNggHdTzxUA', e uma hash-source como 'sha256-abcd...'. As palavras-chave vêm entre aspas na própria gramática: keyword-source = "'self'" / "'unsafe-inline'" / "'unsafe-eval'" / "'strict-dynamic'" / "'unsafe-hashes'" / .... Escrito sem as aspas, self não é uma palavra-chave que falhou; é uma host-source que deu certo, um host chamado self, e unsafe-inline é um host chamado unsafe-inline. A política é analisada, o console fica quieto, e o script inline é bloqueado enquanto o autor acredita que foi permitido, ou self não casa enquanto o autor acredita que casou.

Dois fatos a mais da gramática explicam surpresas comuns. 'none' é uma produção própria: serialized-source-list = ( source-expression *( required-ascii-whitespace source-expression ) ) / "'none'"; ao lado de outras expressões não casa com nada e não muda nada. E "Though IP address do match the grammar above, only 127.0.0.1 will actually match a URL when used in a source expression", porque o casamento de host começa com "If host is not a domain, return Does Not Match" (6.7.2.10). Uma política que permite 10.0.0.1 não permite nada.

Passo três: a cadeia de recurso, e a herança que não existe

default-src "serves as a fallback for the other fetch directives" (6.1.3), e a especificação dá a ordem exata por destino em "Get fetch directive fallback list" (6.8.3): elementos script olham para script-src-elem, depois script-src, depois default-src; manipuladores de evento inline para script-src-attr, depois script-src, depois default-src; workers para worker-src, child-src, script-src, default-src; frames para frame-src, child-src, default-src; e connect-src, img-src, font-src, media-src, manifest-src e object-src recorrem só a default-src.

A frase mais mal lida vem a seguir: "There is no inheritance. If a script-src directive is explicitly specified, for example, then the value of default-src has no influence on script requests." Uma política que diz default-src 'none'; script-src https://cdn.example.com não significa "nada, exceto aquela rede de distribuição de conteúdo () para scripts"; significa scripts da CDN e de nenhum outro lugar, e para script o 'none' nem é consultado. Três diretivas nunca recorrem: base-uri, form-action e frame-ancestors. A especificação é explícita sobre a última: "a policy that declares default-src 'none' will still allow the resource to be embedded by anyone" (6.4.2). A proteção contra clickjacking precisa ser escrita por extenso.

Passo quatro: o que casa com o quê

O curinga é mais estreito do que parece. Para *, o algoritmo de casamento (6.7.2.8) devolve um casamento só se "url's scheme is an HTTP(S) scheme" ou "url's scheme is the same as origin's scheme"; URLs data: e blob: não casam com * a menos que escritas por extenso, "e.g. default-src * data: custom-scheme-1: custom-scheme-2:". Esquemas sobem, mas nunca descem: "script-src http: is treated as equivalent to script-src http: https:" (6.7.2.9), e um host escrito sem esquema casa com o esquema da página e a sua elevação segura. 'self' casa com a origem da página e, dentro de limites, com as suas formas https e wss na porta padrão ou igual; nunca casa com blob: (6.7.2.8).

Então a regra que decide a maioria das auditorias. "A source list allows all inline behavior of a given type if it contains the keyword-source expression 'unsafe-inline', and does not override that expression" (6.7.3.2), e a anulação é qualquer nonce ou hash na mesma lista, ou, para script, 'strict-dynamic': "If expression matches the nonce-source or hash-source grammar, return Does Not Allow". Então script-src 'unsafe-inline' 'nonce-abc' não permite script inline em nenhum navegador que entenda nonces; a palavra-chave é um recurso para os navegadores que não entendem. A especificação mostra a escada pretendida: a política 'unsafe-inline' https: 'nonce-abcdefg' 'strict-dynamic' "will act like 'unsafe-inline' https: in browsers that support CSP1, https: 'nonce-DhcnhD3khTMePgXwdayK9BsMqXjhguVV' in browsers that support CSP2, and 'nonce-DhcnhD3khTMePgXwdayK9BsMqXjhguVV' 'strict-dynamic' in browsers that support CSP3" (8.2).

O que 'strict-dynamic' desliga

"If present in a script-src or default-src directive, it has two main effects: host-source and scheme-source expressions, as well as the 'unsafe-inline' and 'self' keyword-sources will be ignored when loading script. hash-source and nonce-source expressions will be honored. Script requests which are triggered by non-'parser-inserted' script elements are allowed" (8.2). O primeiro efeito é por que uma lista de hosts e 'strict-dynamic' na mesma lista não é uma contradição, mas uma escada; é também por que 'strict-dynamic' sem nonce nem hash não deixa nada que possa rodar. O segundo efeito é o ponto: um script em que a página confiou por nonce pode carregar as suas próprias dependências com document.createElement('script'), e elas rodam porque o script confiável as criou, não porque o host delas foi listado. A especificação é franca sobre a razão de isso existir: "Host- and path-based policies are tough to get right, especially on sprawling origins like CDNs", e as listas que resultam "end up being brittle, awkward, and difficult to implement and maintain".

A política que a especificação chama de Strict CSP

A seção 8.5 nomeia o resultado de tudo isso: "enforcing the following set of CSP directives has been identified as an effective and deployable mitigation against . script-src: Only use nonce source-expression and/or hash source-expression with the 'strict-dynamic' keyword-source. ... base-uri: Specify a value of either 'self' or 'none'." Os seus dois exemplos são o todo: script-src 'strict-dynamic' 'nonce-{RANDOM}'; base-uri 'self'; e script-src 'strict-dynamic' 'sha256-{HASHED_INLINE_SCRIPT}'; base-uri 'self';. O nonce tem a sua própria regra: "the server MUST generate a unique value each time it transmits a policy. The generated value SHOULD be at least 128 bits long (before encoding), and SHOULD be generated via a cryptographically secure random number generator" (7.1), porque "Nonces override the other restrictions present in the directive in which they're delivered." Acrescente object-src 'none' para fechar o sumidouro de plugins que a especificação descreve em 6.1.9, frame-ancestors no cabeçalho (é uma das três diretivas que um elemento meta não pode carregar, 3.3) e report-to para que as violações sejam ouvidas, e a política fica tão forte quanto este mecanismo permite. Tudo o mais num cabeçalho é lido, pelo navegador, exatamente como descrito acima, e o avaliador o lê do mesmo modo.

Fontes