Uploads de arquivos seguros em aplicações web exigem permissões rígidas, limites de tamanho, URLs assinadas e padrões simples de varredura de malware para evitar incidentes.

Uploads de arquivos parecem inofensivos: uma foto de perfil, um PDF, uma planilha. Mas frequentemente são o primeiro incidente de segurança porque permitem que estranhos enviem ao seu sistema uma caixa misteriosa. Se você a aceita, armazena e mostra para outras pessoas, criou uma nova forma de atacar seu app.
O risco não é apenas “alguém enviou um vírus”. Um upload malicioso pode vazar arquivos privados, estourar sua conta de armazenamento ou enganar usuários para entregar credenciais. Um arquivo chamado “invoice.pdf” pode não ser um PDF. Mesmo PDFs e imagens reais podem causar problemas se seu app confiar em metadados, gerar pré-visualizações automaticamente ou servir com regras erradas.
Falhas reais costumam parecer com isto:
Um detalhe causa muitos incidentes: armazenar arquivos não é o mesmo que servir arquivos. Armazenamento é onde você guarda os bytes. Servir é como esses bytes são entregues a navegadores e apps. As coisas dão errado quando um app serve uploads de usuários com o mesmo nível de confiança e regras do site principal, fazendo o navegador tratar o upload como “confiável”.
“Seguro o bastante” para um app pequeno ou em crescimento normalmente significa responder quatro perguntas sem rodeios: quem pode enviar, o que você aceita, qual o tamanho e a frequência, e quem pode ler depois. Mesmo se você estiver construindo rápido (com código gerado ou plataforma guiada por chat), essas salvaguardas continuam importantes.
Trate todo upload como entrada não confiável. A maneira prática de mantê-los seguros é imaginar quem pode abusar deles e o que significa “sucesso” para esse atacante.
A maioria dos atacantes são bots procurando formulários fracos de upload ou usuários reais que tentam estender limites para obter armazenamento grátis, raspar dados ou trollar o serviço. Às vezes é um concorrente sondando por vazamentos ou quedas.
O que eles querem? Normalmente um destes resultados:
Depois, mapeie os pontos fracos. O endpoint de upload é a porta da frente (arquivos muito grandes, formatos estranhos, altas taxas de requisições). O armazenamento é a sala dos fundos (buckets públicos, permissões erradas, pastas compartilhadas). As URLs de download são a saída (previsíveis, de longa duração ou não vinculadas a um usuário).
Exemplo: um recurso de “upload de currículo”. Um bot envia milhares de PDFs grandes para aumentar custos, enquanto um usuário abusa enviando um HTML e compartilha como “documento” para enganar outros.
Antes de adicionar controles, decida o que importa mais para seu app: privacidade (quem pode ler), disponibilidade (se você consegue continuar servindo), custo (armazenamento e banda) e conformidade (onde os dados ficam e por quanto tempo). Essa lista de prioridades mantém decisões consistentes.
A maioria dos incidentes com uploads não é um hack sofisticado. São bugs simples do tipo “eu consigo ver o arquivo de outra pessoa”. Trate permissões como parte do upload, não como algo que você coloca depois.
Comece com uma regra: negar por padrão. Presuma que todo objeto enviado é privado até você permitir explicitamente. “Privado por padrão” é uma base forte para faturas, arquivos médicos, documentos de conta e qualquer coisa vinculada a um usuário. Torne arquivos públicos apenas quando o usuário claramente espera isso (como um avatar público) e, mesmo assim, considere acesso por tempo limitado.
Mantenha papéis simples e separados. Uma divisão comum é:
Não confie em regras por pasta como “tudo em /user-uploads/ está ok.” Verifique propriedade ou acesso por tenant na hora da leitura, para cada arquivo. Isso te protege quando alguém troca de equipe, sai da organização ou um arquivo é reatribuído.
Um padrão de suporte bom é estreito e temporário: conceda acesso a um arquivo específico, registre a ação e expire a permissão automaticamente.
A maioria dos ataques começa com um truque simples: um arquivo que parece seguro por causa do nome ou de um header do navegador, mas que na verdade é outra coisa. Trate tudo que vem do cliente como não confiável.
Comece com uma allowlist: decida os formatos exatos que você aceita (por exemplo, .jpg, .png, .pdf) e rejeite todo o resto. Evite “qualquer imagem” ou “qualquer documento” a menos que realmente precise.
Não confie na extensão do nome nem no header Content-Type do cliente. Ambos são fáceis de falsificar. Um arquivo chamado invoice.pdf pode ser um executável, e Content-Type: image/png pode ser mentira.
Uma abordagem mais forte é inspecionar os primeiros bytes do arquivo, chamados “magic bytes” ou assinatura do arquivo. Muitos formatos comuns têm cabeçalhos consistentes (como PNG e JPEG). Se o cabeçalho não bater com o que você permite, rejeite.
Uma configuração prática de validação:
Renomear importa mais do que parece. Se você armazena nomes fornecidos pelo usuário diretamente, convida truques de caminho, caracteres estranhos e sobregravações acidentais. Use um ID gerado para armazenamento e mantenha o nome original apenas para exibição.
Para fotos de perfil, aceite apenas JPEG e PNG, verifique cabeçalhos e remova metadados se possível. Para documentos, considere limitar a PDF e rejeitar tudo que tenha conteúdo ativo. Se decidir aceitar SVG ou HTML no futuro, trate-os como potencialmente executáveis e isole-os.
A maioria das quedas por upload não são “truques sofisticados”. São arquivos gigantes, muitas requisições ou conexões lentas que ocupam servidores até o app parecer indisponível. Trate cada byte como custo.
Escolha um tamanho máximo por recurso, não um número global. Um avatar não precisa do mesmo limite que um documento fiscal ou um vídeo curto. Defina o menor limite que ainda pareça normal, e adicione um caminho separado para “upload grande” só quando realmente necessário.
Aplique limites em mais de uma camada, porque clientes mentem: na lógica do app, no servidor web ou proxy reverso, com timeouts de upload e rejeição antecipada quando o tamanho declarado é muito grande (antes de ler o corpo inteiro).
Exemplo concreto: avatares com limite de 2 MB, PDFs até 20 MB, e qualquer coisa maior requer um fluxo diferente (como upload direto para object storage com URL assinada).
Mesmo arquivos pequenos podem virar DoS se alguém os enviar em loop. Adicione limites de taxa nos endpoints de upload por usuário e por IP. Considere limites mais rígidos para tráfego anônimo do que para usuários autenticados.
Uploads retomáveis ajudam usuários reais em redes ruins, mas o token de sessão deve ser restrito: expiração curta, vinculado ao usuário e atrelado a um tamanho e destino específicos. Caso contrário, endpoints de “resume” viram um cano livre para seu armazenamento.
Ao bloquear um upload, retorne erros claros para o usuário (arquivo muito grande, muitas requisições), mas não vaze internos (stack traces, nomes de buckets, detalhes de fornecedores).
Uploads seguros não são só sobre o que você aceita. São também sobre onde o arquivo vai e como você o entrega depois.
Mantenha os bytes fora do seu banco de dados principal. A maioria dos apps só precisa de metadados no DB (ID do dono, nome original, tipo detectado, tamanho, checksum, chave de armazenamento, hora de criação). Armazene os bytes em object storage ou um serviço de arquivos feito para blobs grandes.
Separe arquivos públicos e privados no nível de armazenamento. Use buckets ou containers diferentes com regras distintas. Arquivos públicos (como avatares públicos) podem ser legíveis sem login. Arquivos privados (contratos, faturas, documentos médicos) nunca devem ser legíveis publicamente, mesmo que alguém adivinhe a URL.
Evite servir arquivos de usuário no mesmo domínio do seu app quando possível. Se um arquivo arriscado escapar (HTML, SVG com scripts ou problemas de sniffing de MIME), hospedá-lo no domínio principal pode levar a takeover de conta. Um domínio dedicado de download (ou domínio do serviço de armazenamento) limita a área afetada.
Nos downloads, force cabeçalhos seguros. Defina um Content-Type previsível com base no que você permite, não no que o usuário diz. Para qualquer coisa que o navegador possa interpretar, prefira enviar como download.
Algumas configurações padrão que evitam surpresas:
Content-Disposition: attachment para documentos.Content-Type seguro (ou application/octet-stream).Retenção também é segurança. Delete uploads abandonados, remova versões antigas após substituição e defina limites de tempo para arquivos temporários. Menos dados armazenados significa menos para vazar.
URLs assinadas (pre-signed URLs) são uma maneira comum de permitir que usuários enviem ou baixem arquivos sem tornar o bucket público e sem enviar cada byte pela sua API. A URL carrega permissão temporária e depois expira.
Dois fluxos comuns:
Upload direto reduz carga na API, mas torna as regras de armazenamento e restrições da URL mais importantes.
Trate uma URL assinada como uma chave de uso único. Torne-a específica e de curta duração.
Um padrão prático é criar um registro de upload primeiro (status: pending), depois emitir a URL assinada. Após o upload, confirme que o objeto existe e corresponde ao tamanho e tipo esperados antes de marcar como pronto.
Um fluxo de upload seguro é, na maior parte, regras claras e estado bem definido. Trate todo upload como não confiável até que as checagens sejam concluídas.
Escreva o que cada funcionalidade permite. Uma foto de perfil e um documento fiscal não devem compartilhar os mesmos tipos de arquivo, limites de tamanho ou visibilidade.
Defina tipos permitidos e um limite de tamanho por funcionalidade (por exemplo: fotos até 5 MB; PDFs até 20 MB). Faça cumprir as mesmas regras no backend.
Crie um “registro de upload” antes de os bytes chegarem. Armazene: dono (usuário ou org), propósito (avatar, fatura, anexo), nome original, tamanho máximo esperado e um status como pending.
Faça o upload para um local privado. Não deixe o cliente escolher o caminho final.
Valide novamente no servidor: tamanho, magic bytes/tipo, allowlist. Se passar, mude o status para uploaded.
Escaneie por malware e atualize o status para clean ou quarantined. Se a varredura for assíncrona, mantenha o acesso bloqueado enquanto espera.
Permita download, preview ou processamento somente quando o status for clean.
Pequeno exemplo: para uma foto de perfil, crie um registro vinculado ao usuário com propósito avatar, armazene privadamente, confirme que é realmente JPEG/PNG (não apenas nomeado como tal), escaneie e então gere uma URL de pré-visualização.
A varredura é uma camada de segurança, não uma garantia. Ela pega arquivos conhecidos e truques óbvios, mas não detecta tudo. O objetivo é reduzir risco e tornar arquivos desconhecidos inofensivos por padrão.
Um padrão confiável é quarentena primeiro. Salve cada novo upload em um local privado e marque como pendente. Só após as checagens você move para um local “limpo” (ou marca como disponível).
Varreduras síncronas funcionam apenas para arquivos pequenos e baixo tráfego porque o usuário espera. A maioria dos apps faz varredura de forma assíncrona: aceita o upload, retorna um estado “processando” e escaneia em background.
A varredura básica normalmente é um motor de antivírus (ou serviço) mais alguns guardrails: escaneamento AV, checagens de tipo (magic bytes), limites de arquivos compactados (zip bombs, zips aninhados, tamanho descomprimido enorme) e bloqueio de formatos desnecessários.
Se o scanner falhar, expirar ou retornar “desconhecido”, trate o arquivo como suspeito. Mantenha em quarentena e não forneça link de download. É aí que times se queimam: “scan falhou” nunca deve virar “manda assim mesmo”.
Ao bloquear um arquivo, mantenha a mensagem neutra: “Não conseguimos aceitar este arquivo. Tente outro arquivo ou contate o suporte.” Não afirme que detectou malware a menos que tenha certeza.
Considere duas funcionalidades: foto de perfil (mostrada publicamente) e recibo em PDF (privado, usado para cobrança ou suporte). Ambas são problemas de upload, mas não devem compartilhar as mesmas regras.
Para foto de perfil, mantenha regras estritas: permita apenas JPEG/PNG, limite de tamanho (por exemplo 2–5 MB) e re-encode no servidor para não servir os bytes originais do usuário. Armazene publicamente só depois das checagens.
Para o recibo em PDF, permita tamanho maior (por exemplo até 20 MB), mantenha privado por padrão e evite renderizá-lo inline a partir do domínio principal do app.
Um modelo simples de status mantém os usuários informados sem expor detalhes internos:
URLs assinadas se encaixam bem aqui: use uma URL assinada de curta duração para upload (apenas escrita, uma chave de objeto). Emita uma URL assinada de leitura separada e curta apenas quando o status for clean.
Registre o necessário para investigação, não o arquivo em si: ID do usuário, ID do arquivo, tipo estimado, tamanho, chave de armazenamento, timestamps, resultado do scan, IDs de requisição. Evite logar conteúdos brutos ou dados sensíveis dentro de documentos.
A maioria dos bugs de upload acontece porque um atalho “temporário” vira permanente. Presuma que todo arquivo é não confiável, toda URL será compartilhada e toda configuração “a gente arruma depois” será esquecida.
Armadilhas recorrentes:
Content-Type errado, permitindo que o navegador interprete conteúdo arriscado.Monitoramento é o item que as equipes pulam até a conta de armazenamento explodir. Acompanhe volume de uploads, tamanho médio, maiores remetentes e taxas de erro. Uma conta comprometida pode subir milhares de arquivos grandes da noite para o dia.
Exemplo: uma equipe armazena avatares com nomes fornecidos por usuários como “avatar.png” em uma pasta compartilhada. Um usuário sobrescreve imagens de outros. A correção é chata mas efetiva: gere chaves de objeto no servidor, mantenha uploads privados por padrão e exponha uma imagem redimensionada por uma resposta controlada.
Use isto como revisão final antes do deploy. Trate cada item como bloqueador de lançamento, porque a maioria dos incidentes vem de uma salvaguarda faltando.
Content-Type previsível, nomes de arquivos seguros e attachment para documentos.Escreva suas regras em linguagem simples: tipos permitidos, tamanhos máximos, quem pode acessar o quê, quanto tempo URLs assinadas duram e o que significa “scan passou”. Isso vira o contrato compartilhado entre produto, engenharia e suporte.
Adicione alguns testes que peguem falhas comuns: arquivos oversized, executáveis renomeados, leituras não autorizadas, URLs assinadas expiradas e downloads com “scan pendente”. Esses testes são baratos comparados a um incidente.
Se você está construindo e iterando rápido, ajuda usar um fluxo onde possa planejar mudanças e revertê-las com segurança. Times que usam Koder.ai (koder.ai) frequentemente se apoiam em modo de planejamento e snapshots/rollback enquanto apertam regras de upload com o tempo, mas o requisito central continua: a política deve ser aplicada pelo backend, não pela UI.
Comece com privado por padrão e trate todo upload como entrada não confiável. Aplique quatro controles básicos no servidor:
Se você conseguir responder isso de forma clara, já estará à frente da maioria dos incidentes.
Porque usuários podem enviar uma “caixa misteriosa” que seu app armazena e depois pode servir para outros. Isso pode levar a:
Raramente se trata apenas de “alguém enviou um vírus.”
Armazenar é guardar bytes em algum lugar. Servir é entregar esses bytes para navegadores e apps.
O problema acontece quando seu app serve uploads de usuários com as mesmas regras e nível de confiança do site principal. Se um arquivo arriscado for tratado como uma página normal, o navegador pode executá-lo (ou os usuários podem confiar demais nele).
Um padrão mais seguro é: armazene como privado e sirva via respostas controladas com cabeçalhos seguros.
Use negação por padrão e verifique acesso toda vez que um arquivo for baixado ou pré-visualizado.
Regras práticas:
Não confie na extensão do nome do arquivo nem no Content-Type enviado pelo navegador. Valide no servidor:
Quedas de serviço atrapalham porque alguém envia muitos arquivos, arquivos enormes ou conexões lentas que ocupam recursos. Trate cada byte como custo.
Boas práticas:
Resumindo: trate cada requisição como possível abuso.
Sim, mas com cuidado. URLs assinadas permitem que o navegador envie/baixe direto do armazenamento sem tornar o bucket público.
Boas práticas:
Upload direto reduz carga na API, mas exige escopo e expiração rigorosos.
O padrão mais seguro é:
pendingA varredura ajuda, mas não garante 100%. Use-a como rede de segurança, não como único controle.
Abordagem prática:
Política é crucial: “não escaneado” nunca deve significar “disponível”.
Sirva arquivos de forma que impessa navegadores de interpretá-los como páginas web.
Padrões úteis:
Content-Disposition: attachment para documentosA maioria dos bugs reais são erros simples de “consigo ver o arquivo de outro usuário”.
Se os bytes não corresponderem a um formato permitido, rejeite o upload.
cleanquarantinedcleanIsso evita que arquivos “com scan falhando” ou “ainda processando” sejam compartilhados por engano.
Content-Type seguro escolhido pelo servidor (ou application/octet-stream)Isso reduz o risco de um upload virar página de phishing ou execução de script.