Content-Security-Policy: o cabeçalho que trava o XSS que escapou
Escapar à saída é a primeira linha de defesa contra o XSS, e é a que resolve o problema. O Content-Security-Policy é a segunda: serve para o dia em que uma coisa escapou, e o script injetado já está na página a tentar correr. Não impede o furo — impede que o furo sirva para alguma coisa.
É também o único cabeçalho de segurança que consegue partir um site inteiro, e por isso vale a pena ver o caso difícil em vez do exemplo de manual.
1. O que este site já tem
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=()',
}Quatro cabeçalhos, nenhum deles capaz de estragar nada: ou o site já cumpria o que eles dizem, ou não usava aquilo. O Permissions-Policy está lá precisamente para fechar a porta a um script de terceiros — hoje o do AdSense, amanhã outro — que peça câmara ou localização em nome do site.
O CSP não está na lista. E a razão não é esquecimento.
2. O inventário, que é o trabalho todo
Antes de escrever uma política há que saber o que a página carrega. Aqui:
Estilos em linha no conteúdo. O atributo
styleaparece em mais de mil elementos dos artigos herdados de 2009 — parágrafos, imagens, spans, itens de lista. Nenhuma política os aceita semstyle-src 'unsafe-inline'.Um script em linha que tem de correr primeiro. O do tema, que põe a classe no
<html>antes de a página ser pintada. Tirá-lo traz de volta o clarão branco a cada navegação.AdSense, que carrega scripts a partir de si próprio — autorizar o primeiro domínio não basta.
Analytics, de outro domínio ainda.
Iframes do YouTube, nos artigos de vídeo, que obrigam a um
frame-src.Imagens de dezenas de domínios de fora, o que deixa o
img-srcpraticamente aberto.
Isto é o cenário difícil inteiro: conteúdo antigo que não se pode reescrever, e três terceiros que se querem manter. Uma política escrita de cabeça parte isto todo na primeira página.
3. Por isso se começa em modo de relatório
{ key: 'Content-Security-Policy-Report-Only', value: politica }Com -Report-Only o browser avalia a política, comunica cada violação, e não bloqueia nada. Põe-se a correr sobre tráfego real durante uma semana — não sobre o que se imagina que o site carrega — e lê-se o que aparece. É a única forma honesta de descobrir o que falta, porque nenhum inventário feito à mão fica completo.
E com a armadilha dita à partida: o modo de relatório nunca parte nada, portanto nunca cria pressa. É muito comum ficar ligado dois anos a comunicar violações para um sítio que ninguém lê. O modo de relatório é um passo, não um estado.
4. O nonce, e porque é que aqui dá
Para o script do tema há duas saídas sem abrir a porta a todos: o hash do seu conteúdo, ou um nonce — um número usado uma vez, diferente em cada resposta, posto no cabeçalho e no <script>.
O nonce tem um requisito que se percebe mal: tem de ser diferente a cada resposta. Num HTML guardado em cache e servido mil vezes, o mesmo nonce vai com ele mil vezes, e deixa de valer alguma coisa. Aqui as páginas são renderizadas a cada pedido, portanto dá; num site estático não daria, e o hash passaria a ser a única resposta certa.
5. Duas coisas que não vale a pena fazer
A primeira é resolver o problema com script-src 'unsafe-inline'. Isso cancela o cabeçalho: a política passa a autorizar exactamente aquilo de que se estava à espera — script injetado a correr dentro da página. Um hash para cada script conhecido dá mais trabalho e é a diferença entre ter CSP e ter a aparência de CSP.
A segunda é apagar o X-Frame-Options depois de pôr frame-ancestors. O moderno substitui o antigo nos browsers que o entendem, e não há vantagem nenhuma em retirar o antigo: ficam os dois, que é um caso raro de redundância que não custa nada.
6. A ordem por que isto se faz
Aqui ainda não está feito, e a ordem é esta: montar a política em modo de relatório; deixá-la correr uma semana sobre tráfego real; dar nonce ao script do tema; aceitar o 'unsafe-inline' nos estilos como dívida do conteúdo antigo, documentada e não escondida; e só então trocar o cabeçalho pelo que bloqueia.
É deliberadamente lento. Um CSP ligado de uma vez num site com três terceiros e mil atributos style descobre-se ao domingo, quando alguém diz que as imagens desapareceram.
O resumo
O CSP não substitui escapar à saída, que continua a ser onde o XSS se resolve. O que ele faz é reduzir o estrago do que passou — e é um cabeçalho que se mede em inventário, não em configuração. Escrever a política leva dez minutos; saber o que a página carrega leva uma semana, e é esse o trabalho.