A política que este app já tem
A maioria das aplicações Rails tem o config/initializers/content_security_policy.rb ali, inteiramente comentado. Esta não.
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.font_src :self, :https, :data, "https://fonts.bunny.net"
policy.img_src :self, :https, :data, :blob
policy.object_src :none
# wasm-unsafe-eval is required for the ruby.wasm browser sandbox (WebAssembly.compile)
policy.script_src :self, :wasm_unsafe_eval, "https://cdn.jsdelivr.net"
policy.style_src :self, :unsafe_inline, "https://fonts.bunny.net", "https://cdn.jsdelivr.net"
policy.connect_src :self, "https://cdn.jsdelivr.net", "https://ga.jspm.io", "https://fonts.bunny.net", :blob
policy.worker_src :self, :blob
end
config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
config.content_security_policy_nonce_directives = %w[script-src]
end
Cada linha disso é uma decisão que alguém tomou, e este curso é sobre lê-las.
O que é uma política
Um cabeçalho de resposta dizendo ao navegador de onde os recursos podem vir:
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://cdn.jsdelivr.net; object-src 'none'
O navegador impõe. Um <script src="https://evil.example/x.js"> injetado na sua página (por um XSS armazenado, uma dependência comprometida, uma extensão de navegador) não carrega, e o console registra uma violação.
Esse é o enquadramento a segurar: CSP não é um jeito de evitar XSS. É um jeito de fazer um XSS não funcionar. A injeção ainda acontece; o payload não executa. É defesa em profundidade, e é a camada que pega o bug que você não achou.
flowchart TB
X["Uma injeção dá certo: tag de script na página"] --> B["O navegador lê o cabeçalho de CSP"]
B --> Q{"o script-src permite esta origem?"}
Q -->|"não"| BL["não executa; violação reportada"]
Q -->|"sim"| RUN["executa — a CSP não ajudou aqui"]
BL --> D["O XSS existiu e não fez nada"]
Lendo esta política
| Diretiva | Valor aqui | Significa |
|---|---|---|
default_src |
:self |
Tudo que não for especificado: só a mesma origem |
font_src |
:self, :https, :data, bunny.net |
Fontes de qualquer lugar por HTTPS, mais URIs data |
img_src |
:self, :https, :data, :blob |
Imagens de qualquer lugar por HTTPS, mais data e blob |
object_src |
:none |
Nenhum <object>, <embed> ou applet |
script_src |
:self, :wasm_unsafe_eval, jsdelivr |
Scripts próprios, compilação WebAssembly, e uma CDN |
style_src |
:self, :unsafe_inline, duas CDNs |
Estilos próprios, estilos embutidos, e duas CDNs |
connect_src |
:self mais três hosts e :blob |
Para onde fetch e XMLHttpRequest podem ir |
worker_src |
:self, :blob |
De onde Web Workers podem ser carregados |
Duas dessas são incomuns e as duas têm motivo, que a aula 4 percorre em detalhe. O wasm_unsafe_eval existe para a prévia em ruby.wasm que mostra ao aluno a saída do código no navegador (a correção roda no servidor), e o unsafe_inline no style_src é a linha mais frouxa do arquivo.
O nonce
config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
config.content_security_policy_nonce_directives = %w[script-src]
Um nonce é um token que o servidor coloca no cabeçalho e na tag de script. O navegador roda um script embutido só quando os dois casam. Então um script injetado por quem ataca (que não conhece o token) não roda mesmo sendo embutido.
No layout:
<script nonce="<%= content_security_policy_nonce %>">
if (navigator.serviceWorker) {
navigator.serviceWorker.register("/service-worker", { scope: "/" });
}
</script>
Esse é o mecanismo funcionando como projetado: um script embutido, permitido pelo nome, sem nenhum unsafe-inline no script_src.
O gerador merece uma nota. O request.session.id.to_s é o gerador que o próprio Rails sugere, e significa que o nonce é estável por sessão em vez de novo a cada resposta. A propriedade de segurança continua valendo contra quem ataca de fora, que não tem o id da sessão. E é mais fraca que um valor aleatório por resposta, porque qualquer coisa que vaze o id da sessão vaza o nonce. A aula 3 volta a isso.
E o csp_meta_tag
<%= csp_meta_tag %>
Isso emite o nonce numa meta tag para que JavaScript que acrescenta um elemento de script em tempo de execução (Turbo, importmap) consiga lê-lo e marcar o que criar como permitido. Sem isso, scripts inseridos dinamicamente são bloqueados, e o único sintoma é um erro no console e um recurso que não faz nada, em silêncio.
O que este curso cobre
- A política que este app já tem (esta aula).
- Diretivas e a cadeia de fallback.
- Nonces, hashes e
strict-dynamic. - As exceções desta política.
- Implantação, e o modo só de relatório.
- Além do
script-src. - Juntando tudo: endurecendo esta política.
Pontos-chave
- Este repositório tem uma CSP ativa, diferente da maioria das aplicações Rails em que o inicializador está comentado.
- CSP não evita uma injeção; ela impede o payload injetado de executar.
default_src :selfé o fallback, e cada diretiva específica o sobrepõe para o tipo de recurso dela.- Um nonce deixa um script embutido rodar pelo nome; esta política o usa no registro do service worker.
- O gerador de nonce é por sessão, que é a sugestão do próprio Rails e é mais fraca que um valor por resposta.
Continue com a aula 2
Diretivas e a cadeia de fallback
Explica como diretivas de busca substituem o default-src, quais três não têm fallback e o que significam os valores de origem.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.