Adriano Júnior
← Artigos

Segurança

Pare de expor seu bucket! (S3 AWS, R2 Cloudflare e outros….)

Por que tratar o storage como “invisível” falha na prática: URLs diretas expõem padrões de nomes, estrutura de diretórios e superfície para scraping e enumeração — e como proxyar via backend com blob no seu domínio.

Um erro bem comum em aplicações que lidam com arquivos é tratar o storage como se fosse “invisível”. Na prática, muita gente entrega arquivo assim:

https://bucket.s3.amazonaws.com/arquivo.pdf ou alguma variação disso com CDN ou signed URL…

Funciona? Sim. Mas tem um detalhe importante: você está expondo diretamente uma parte sensível da sua infraestrutura, e isso abre algumas superfícies que normalmente passam despercebidas.

O problema não é só o acesso ao arquivo. Quando você expõe o bucket, a depender da sua configuração você pode estar revelando:

  • Padrões de nomes (ex.: user123/invoice, user1234/invoice).
  • Estrutura de diretórios (users/upload/profile/photo.png).
  • Existência de mais arquivos (invoice2024, invoice2025, invoice2026).
  • Comportamento de storage (headers, respostas, etc.).

Entendi, mas qual o problema disso?

Com as informações acima, um atacante pode, com facilidade:

  • Tentar adivinhar outros arquivos.
  • Automatizar scraping.
  • Mapear como sua aplicação organiza dados.
  • Explorar possíveis falhas de permissão.

Não é um ataque sofisticado: é exploração básica de uma superfície exposta.

Manter assim é como deixar a porta da sua casa aberta e dizer: pode entrar, vá até a dispensa, pega o que precisa, só não mexe no resto.

“Mas eu uso signed URL na minha aplicação”

Você reduz o risco, mas ainda fica exposto. Signed URL não elimina o problema porque:

  • A origem ainda fica visível.
  • O padrão de URLs continua exposto.
  • Ainda existe uma janela de exploração enquanto a URL está válida.

Agora sua porta tem chave temporária, e você diz: pode entrar na dispensa só por cinco minutos. A pessoa ainda sabe onde fica a casa, ainda pode compartilhar a chave com terceiros e ainda pode voltar enquanto a chave for válida.

A mudança de abordagem

Em vez de deixar o storage “visível”, a ideia é simples: o cliente não deve saber que o bucket existe.

Fluxo enxuto:

  1. Cliente faz uma requisição autenticada.
  2. Backend valida acesso.
  3. Backend busca o arquivo no storage.
  4. Frontend consome e expõe via blob: sob o seu domínio.

O storage (bucket) vira apenas um detalhe interno.

Backend (Node/Express de exemplo): valida sessão, lê o arquivo no S3/R2 com SDK ou signed URL interna, e devolve o stream com Content-Type adequado.

// Conceito: rota autenticada — o cliente nunca vê a URL do bucket
app.get("/api/files/:id", requireAuth, async (req, res) => {
  const file = await resolveFileForUser(req.user.id, req.params.id);
  if (!file) return res.sendStatus(404);

  const stream = await storage.getObjectStream(file.storageKey);
  res.setHeader("Content-Type", file.mimeType);
  res.setHeader(
    "Content-Disposition",
    `inline; filename="${encodeURIComponent(file.name)}"`,
  );
  stream.pipe(res);
});

Frontend — usar a URL da sua API e gerar blob para exibir ou baixar:

async function openPrivateFile(fileId: string) {
  const res = await fetch(`/api/files/${fileId}`, {
    credentials: "include",
  });
  if (!res.ok) throw new Error("Sem permissão ou arquivo inexistente");

  const blob = await res.blob();
  const url = URL.createObjectURL(blob);
  window.open(url, "_blank", "noopener");
  // Depois: URL.revokeObjectURL(url) conforme o caso
}

Um toque especial para fechar bem

O bucket continua privado. Quando o backend precisa ler do provedor, ele pode usar signed URL só no servidor, com:

  • Validade curta (minutos).
  • Escopo limitado a um único objeto.
  • Bônus: renomear ou rotacionar keys para não vazar padrão.

Essa URL é usada apenas internamente para fetch/stream. O cliente nunca vê — e se vazar por engano, expira rápido e não abre outros arquivos.

O que isso muda na prática

Você reduz bastante a superfície exposta:

  • Não há URL pública óbvia do storage.
  • Não há padrão visível de arquivos na barra de endereços do usuário.
  • Fica mais difícil automatizar enumeração de fora.
  • Toda entrega passa por validação no seu backend.

Não é sobre esconder por esconder — é sobre reduzir superfície de ataque.

Nem tudo são flores

Essa abordagem tem trade-offs, como tudo na programação:

  • Mais tráfego passando pela sua API.
  • Mais carga no backend.
  • Mais complexidade operacional.

Em alguns casos compensa cache (CDN privada, edge) ou fila dedicada para downloads pesados. Algumas equipes separam uma API só de arquivos para não misturar com o core do negócio.

O critério é seu. Em geral, vale especialmente quando você tem:

  • Arquivos privados ou dados sensíveis.
  • Necessidade de não revelar infraestrutura de storage na borda.

Agora que você conhece esse risco silencioso, vale aprofundar e escolher o desenho certo para o seu projeto.