Blog / Segurança

CSP sem 'unsafe-inline' no Astro: 3 armadilhas reais

· 4 min de leitura · Equipe N3XUS

Código-fonte em um editor com tema escuro

Artigo

O site da N3XUS é estático, feito em Astro, e responde com uma Content Security Policy rígida: nenhum script ou estilo de fora do próprio domínio e nenhum estilo embutido no HTML. Parecia resolvido até a auditoria do Lighthouse apontar erros no console de uma única página: um post do blog.

A causa não era nosso código. Eram três recursos do próprio ecossistema que, por padrão, geram estilo inline.

Em resumo

  • O destaque de sintaxe padrão do Markdown no Astro (Shiki) grava style="" nos blocos de código. Trocamos por Prism, que usa classes.
  • O componente <Font /> da API de fontes injeta <style> inline. Usamos o CSS do Fontsource e fazemos o preload manualmente.
  • O Astro embute CSS pequeno no HTML por padrão. Configuramos inlineStylesheets: 'never'.

A política que queríamos manter

O header é enviado pelo nginx, na frente do site:

default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self';
img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self';
frame-ancestors 'self'; upgrade-insecure-requests

Preferimos o header em vez da tag <meta> porque algumas diretivas não funcionam via meta. A documentação da MDN é explícita sobre frame-ancestors, por exemplo. Com style-src 'self', qualquer atributo style="" e qualquer <style> no HTML é bloqueado.

Armadilha 1: destaque de código com estilo inline

Por padrão, o Astro colore os blocos de código Markdown com o Shiki, e as cores vão direto no HTML:

<pre class="astro-code" style="background-color:#24292e;color:#e1e4e8">

O navegador bloqueia esses estilos e registra um erro de CSP no console. Foi isso que derrubou a nota de boas práticas do post de 100 para 92.

A opção defaultColor: false do Shiki não resolve: ela troca as cores por variáveis CSS, mas continua gravando as variáveis num atributo style. A saída foi usar o Prism, que marca o código com classes:

// astro.config.mjs
export default defineConfig({
  markdown: { syntaxHighlight: 'prism' },
});

O Prism não traz tema, então escrevemos o nosso no CSS do site, com meia dúzia de regras para .token.keyword, .token.string e .token.comment.

Armadilha 2: o componente de fontes

A API de fontes do Astro é ótima: baixa as fontes, hospeda no próprio site e gera métricas de fallback. Mas o componente <Font /> injeta as declarações @font-face num <style> dentro do HTML, o que a nossa CSP bloqueia.

Mantivemos as fontes pelo pacote do Fontsource, importado no CSS global, que vira arquivo externo:

@import '@fontsource-variable/geist';
@import '@fontsource-variable/geist-mono';

Isso trouxe outro problema: a troca da fonte de reserva pela fonte final mexia no layout dos títulos grandes. A página de soluções chegou a ter CLS de 0,33 no desktop. A correção foi pré-carregar os arquivos principais, importando a URL com hash direto do pacote:

---
import geist from '@fontsource-variable/geist/files/geist-latin-wght-normal.woff2?url';
---
<link rel="preload" href={geist} as="font" type="font/woff2" crossorigin />

O Vite devolve a mesma URL usada pelo CSS, então o navegador baixa o arquivo uma única vez. O CLS caiu para 0 em todas as páginas.

Armadilha 3: CSS pequeno embutido no HTML

Com a configuração padrão (auto), o Astro coloca folhas de estilo menores que 4 KB direto no HTML, dentro de <style>. É uma otimização de performance que conflita com a CSP. A opção abaixo força tudo para arquivos:

export default defineConfig({
  build: { inlineStylesheets: 'never' },
});

Como detectar antes de publicar

  • Console do navegador. Abra cada tipo de página (home, post com código, página com imagem) e procure “Content Security Policy”.
  • Lighthouse. A auditoria “Erros registrados no console” acusa o bloqueio e derruba a nota de boas práticas.
  • CI. No nosso pipeline, o build falha se algum HTML gerado tiver style=":
- name: Nada de style inline
  run: "! grep -rl ' style=\"' dist --include=*.html"

Também evitamos estilo inline no nosso próprio código: o que antes seria style="order:2" virou uma classe. A única exceção do site é o painel administrativo, que roda um CMS de terceiros e recebe uma CSP própria, mais permissiva, só no caminho /admin/, protegido por login.

Referências

Quer revisar a CSP ou os headers de segurança do seu site? Fale com a gente.