Back to the Blog

Um bug de cache que só aparecia em produção: Vite, SRI e cache imutável

Como um arquivo com o mesmo nome e conteúdo diferente poderia bloquear o site de quem voltasse após uma publicação, e a correção que manteve a verificação de integridade.

Blog posts are only available in Portuguese for now.

Em segurança de front-end, duas boas práticas podem entrar em conflito. A primeira é o cache longo e imutável para arquivos com hash no nome. A segunda é a SRI (Subresource Integrity), que faz o navegador recusar um arquivo cujo conteúdo não confere com o esperado. Este texto descreve um caso em que a combinação das duas, em um projeto Nuxt, criava uma falha que só apareceria em produção.

O cenário

O Nuxt serve os arquivos JavaScript em /_nuxt/ com o hash do conteúdo no nome e, por padrão, cache de um ano (immutable). A lógica é correta: se o conteúdo muda, o nome muda, e o navegador baixa o arquivo novo. A biblioteca de segurança do projeto (nuxt-security) acrescenta a SRI: a página declara o hash esperado de cada arquivo e o navegador o confere antes de executar.

Comparação entre antes e depois da correção do cache: com cache imutável, um arquivo com o mesmo nome e conteúdo diferente bloqueava a página; com revalidação a cada carga, a página abre.

O problema

O Vite calcula o hash do nome de um chunk antes de preencher a lista de dependências dele (__vite__mapDeps). Na prática, isso permite que dois builds produzam um arquivo com o mesmo nome e conteúdo diferente.

A verificação foi feita compilando o projeto, alterando um valor de CSS, compilando de novo e comparando nomes e conteúdos em .output/public/_nuxt. Um dos arquivos manteve o nome com o conteúdo alterado.

O efeito só atingiria quem já tinha visitado o site antes da publicação, o que torna o defeito difícil de notar em desenvolvimento e em visitas novas.

As opções

Duas saídas foram descartadas:

  • Desligar a SRI. Remove o bloqueio, mas também a proteção contra um arquivo alterado no caminho.
  • Manter o cache imutável. Mantém a janela de risco de um ano.

A correção

A solução foi reduzir o cache dos arquivos de /_nuxt/ a 1 segundo, configurado no hook nitro:config do nuxt.config.ts. O navegador passa a revalidar a cada carga: se o arquivo não mudou, o servidor responde 304 e nada é baixado de novo; se mudou, o arquivo novo chega.

hooks: {
  'nitro:config'(nitroConfig) {
    for (const asset of nitroConfig.publicAssets ?? []) {
      if (asset?.baseURL?.startsWith('/_nuxt')) asset.maxAge = 1
    }
  },
},

Como reproduzir o teste

  1. Compilar o projeto (pnpm build).
  2. Alterar uma dependência ou um valor de CSS.
  3. Compilar novamente.
  4. Comparar os nomes e os conteúdos dos arquivos em .output/public/_nuxt.

Se algum nome se repetir com conteúdo diferente, o problema está presente. O procedimento ficou registrado em docs/SECURITY.md, para ser repetido a cada mudança relevante de versão do Vite ou do Nuxt.