Armazenamento de objetos
Onde guardar arquivos, com URLs assinadas e envio direto do navegador.
O problema: onde guardar os arquivos
Os usuários mandam fotos de perfil, notas fiscais, contratos assinados. A primeira ideia costuma ser salvar no disco do servidor. Ela quebra rápido: com duas instâncias da aplicação, cada uma tem o seu disco; num contêiner, o disco some a cada deploy; e o backup precisa de um cuidado à parte. A segunda ideia, guardar o arquivo no banco, enche o banco de bytes que nenhuma consulta usa, e cada backup carrega todos eles.
O armazenamento de objetos resolve isso. Cada arquivo é um objeto, guardado sob uma chave (fotos/981.png) dentro de um bucket, e acessado por HTTP. Não há pastas de verdade, nem edição parcial: um objeto é gravado inteiro e lido inteiro, ou em faixas de bytes. O Amazon S3 criou a interface que quase todos seguem: Google Cloud Storage, Azure Blob Storage, Cloudflare R2 e servidores próprios como Ceph e SeaweedFS.
| Disco do servidor | Banco de dados | Armazenamento de objetos | |
|---|---|---|---|
| Várias instâncias | Cada uma com seu disco | Compartilhado | Compartilhado |
| Sobrevive à troca da instância | Não | Sim | Sim |
| Custo por GB | Médio | Alto | Baixo |
| Entrega direta ao navegador | Pela aplicação | Pela aplicação | Por URL, sem passar pela aplicação |
O banco guarda os dados sobre o arquivo (dono, nome original, tamanho, chave); o armazenamento guarda os bytes.
Os testes desta página usaram o SeaweedFS 4.53, um servidor compatível com a API do S3, e os SDKs oficiais da AWS nas cinco linguagens. O MinIO, mais conhecido para testes locais, não estava mais disponível no Docker Hub no momento do teste.
URLs assinadas
O bucket é privado: um pedido anônimo para um objeto recebeu 403 no teste. Para que o navegador baixe ou envie um arquivo sem receber as credenciais, a aplicação gera uma URL assinada (presigned URL): uma URL comum com uma assinatura que vale só para aquele objeto, aquela operação e por um tempo limitado.
https://armazenamento/arquivos/fotos/1.png
?X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=.../20261115/us-east-1/s3/aws4_request
&X-Amz-Expires=300
&X-Amz-SignedHeaders=content-type;host
&X-Amz-Signature=dff2ecaa...Testamos uma URL de envio válida por 5 segundos, com o tipo image/png incluído na assinatura:
| Tentativa | Resultado |
|---|---|
Enviar um PNG com Content-Type: image/png | 200 |
Enviar HTML com Content-Type: text/html, na mesma URL | 403 |
Trocar a chave na URL para fotos/2.png | 403 |
| Enviar o PNG de novo, 6 segundos depois | 403, a URL venceu |
O fluxo de envio direto
Com URLs assinadas, o arquivo não precisa passar pela aplicação:
- O navegador pede à aplicação uma URL de envio, dizendo o tipo e o tamanho do arquivo.
- A aplicação confere se o usuário pode enviar, escolhe a chave e devolve a URL assinada.
- O navegador envia o arquivo direto para o armazenamento.
- O navegador avisa a aplicação que terminou, e ela confirma que o objeto existe antes de registrar.
A diferença aparece na memória da aplicação. Medimos um servidor Node que recebe um arquivo de 300 MB e o repassa ao armazenamento:
| Forma de repassar | Pico de memória do servidor |
|---|---|
| Juntar o arquivo inteiro na memória e enviar | 716 MB |
| Repassar em fluxo, em partes de 8 MB | 139 MB |
| Envio direto do navegador com URL assinada | Nenhum: o arquivo não passa pela aplicação |
Dez usuários enviando vídeos ao mesmo tempo, com a primeira forma, somam gigabytes de memória. O envio direto também tira o tráfego da aplicação e não ocupa uma conexão dela pelo tempo inteiro do envio.
O que a assinatura precisa fixar
Uma URL de envio é uma permissão dada ao navegador do usuário, e o usuário pode usá-la como quiser. O que não estiver fixado na assinatura fica por conta dele.
O tipo do arquivo. Uma URL assinada sem o tipo aceitou, no teste, um HTML gravado como fotos/3.png, com Content-Type: text/html. Na leitura, o armazenamento devolveu Content-Type: text/html. Se esse arquivo for servido a partir do domínio do sistema, o navegador o abre como uma página, com acesso aos cookies do domínio: um XSS armazenado, só com um "upload de foto". Duas defesas, as duas testadas:
- Assinar o tipo: com
image/pngna assinatura, o envio detext/htmlrecebeu 403. - Forçar a resposta na URL de download (
response-content-disposition=attachmenteresponse-content-type=application/octet-stream): o mesmo arquivo foi entregue comoapplication/octet-stream, para baixar, e não como página.
O tamanho. Uma URL de envio por PUT, pensada para fotos de perfil, aceitou um arquivo de 20 MB: a assinatura de um PUT não limita o tamanho. O envio por POST com uma política (POST policy) limita: com content-length-range de até 1 MB, um arquivo de 500 KB foi aceito (204) e um de 2 MB recusado com EntityTooLarge.
Por que isso importa quando a IA escreve o código
Um assistente que implementa "upload de arquivo" costuma escolher o caminho mais curto:
- Salvar no disco do servidor, ou em uma coluna do banco.
- Receber o arquivo inteiro na memória da aplicação antes de enviá-lo ao armazenamento.
- Buckets públicos, para "facilitar" a exibição.
- URLs assinadas sem o tipo e sem limite de tamanho, ou com validade de dias.
- Arquivos enviados por usuários servidos do próprio domínio, com o tipo que o usuário escolheu.
- O nome original do arquivo usado como chave, com colisões e caracteres problemáticos.
O próximo nível compara as formas de envio e de entrega, e as configurações de bucket.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Armazenamento de objetos em cinco minutos
Explique em no máximo cinco minutos de leitura o que é armazenamento de objetos, como o Amazon S3, para alguém que sempre salvou arquivos no disco do servidor. Cubra só o essencial: objetos, chaves e buckets, por que não guardar arquivos no disco nem no banco, URLs assinadas e o envio direto do navegador. Use o exemplo de fotos de perfil e notas fiscais. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
URLs assinadas por dentro
Quero entender como funcionam as URLs assinadas do S3. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. O que entra na assinatura: método, chave, validade e cabeçalhos assinados. 2. Por que trocar a chave ou o tipo invalida a URL. 3. A diferença entre assinar um PUT e uma política de POST. 4. Como forçar o tipo e o Content-Disposition da resposta num download. 5. Por que a validade deve ser curta, e o que acontece com uma URL vazada. Em cada etapa, me mostre uma URL ou um pedido e peça que eu diga se ele seria aceito.
Os riscos do upload de arquivos
Quero entender os riscos de segurança de receber arquivos dos usuários. Explique, com um exemplo de ataque para cada um: 1. HTML ou SVG enviado como imagem e servido do domínio do sistema (XSS armazenado). 2. Arquivos enormes enchendo o armazenamento e a conta. 3. Chaves previsíveis e acesso a arquivos de outros usuários. 4. Buckets públicos por engano. 5. Arquivos com conteúdo malicioso para quem vai abri-los. Para cada um, diga a defesa e como testar.
Envio pela aplicação ou direto
Quero entender as formas de receber arquivos dos usuários. Compare, com o fluxo de cada uma: 1. Receber na aplicação em memória. 2. Receber na aplicação e repassar em fluxo. 3. Envio direto do navegador com URL assinada por PUT. 4. Envio direto com POST e política. 5. Envio em partes para arquivos grandes. Explique o efeito de cada forma na memória e nas conexões da aplicação, e depois me descreva três funcionalidades e pergunte qual forma eu usaria.