Envios de arquivos seguros: permissões, limites, URLs assinadas, varreduras
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.

Por que uploads de arquivos são arriscados (em linguagem simples)
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:
- Alguém adivinha uma URL de arquivo e baixa o documento de outro usuário.
- Um arquivo HTML enviado é servido como página web e mostra um prompt que rouba credenciais.
- Um atacante envia arquivos enormes repetidamente até que seu app fique lento ou caia.
- Um tipo “seguro” é falsificado e aberto por funcionários numa máquina interna.
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.
Um modelo de ameaça simples para uploads
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:
- Executar código nos seus servidores ao enviar algo que é executado.
- Roubar arquivos privados adivinhando, reutilizando ou compartilhando URLs de download.
- Prejudicar a disponibilidade enchendo uploads ou forçando processamento caro.
- Inflar sua conta com crescimento de armazenamento ou downloads pesados.
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.
Permissões e controle de acesso que realmente funcionam
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.
Papéis que correspondem ao trabalho real
Mantenha papéis simples e separados. Uma divisão comum é:
- Uploader: pode criar uploads para sua própria conta
- Visualizador: pode baixar arquivos que tem permissão para ver
- Suporte: acessa arquivos apenas com concessão temporária e auditável
- Admin: gerencia políticas, mas não deve ler tudo automaticamente
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.
Validando tipos de arquivo sem confiar no cliente
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:
- Allowlist das extensões aceitas (lista no servidor).
- Detectar o MIME type no servidor (não confiar no header do cliente).
- Verificar magic bytes para os formatos suportados.
- Gerar um nome de armazenamento aleatório e guardar o nome original como metadado.
- Bloquear formatos arriscados a menos que realmente precise, especialmente HTML, SVG e conteúdos tipo script.
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.
Limites de tamanho, limites de taxa e noções básicas de DoS
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.
Defina limites de tamanho onde eles realmente funcionam
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).
Limites de taxa e controles de abuso
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).
Escolhas seguras de armazenamento e entrega
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:
- Use
Content-Disposition: attachmentpara documentos. - Use um
Content-Typeseguro (ouapplication/octet-stream). - Armazene e sirva com chaves de objeto opacas (não nomes de usuário).
- Registre downloads de arquivos privados.
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: quando usar e como mantê-las seguras
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 para armazenamento: seu app emite uma URL assinada de curta duração e o navegador faz upload direto para o object storage.
- Upload via servidor: o arquivo passa pela sua API primeiro e então seu servidor armazena.
Upload direto reduz carga na API, mas torna as regras de armazenamento e restrições da URL mais importantes.
Como manter URLs assinadas restritas
Trate uma URL assinada como uma chave de uso único. Torne-a específica e de curta duração.
- Expire URLs de escrita rapidamente (frequentemente 1–5 minutos). Mantenha URLs de leitura por minutos, não dias.
- Vincule a URL à chave de objeto exata esperada (um objeto, não uma pasta).
- Adicione restrições onde suportado: tipo de conteúdo esperado, tamanho máximo, checksum.
- Emita URLs apenas após checagens de permissão.
- Registre quem solicitou a URL e por quê (ID do usuário, chave do objeto, propósito, IP/user agent).
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.
Passo a passo: um fluxo de upload seguro que você pode implementar
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.
Um fluxo prático (com status reais)
-
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
cleanouquarantined. 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.
Padrões básicos de varredura de malware (sem prometer demais)
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.
O que uma “varredura básica” geralmente inclui
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.
Exemplo: upload de foto de perfil e documento em um app típico
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:
- pending: usuário escolheu um arquivo, upload não iniciado
- uploaded: o armazenamento recebeu os bytes
- scanning: job em background está checando
- clean (ou rejected): arquivo disponível (ou bloqueado)
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.
Erros comuns e armadilhas fáceis
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:
- Confiar apenas em checagens do cliente. Navegadores são contornáveis em segundos.
- Permitir que usuários influenciem caminhos, nomes de arquivos ou chaves de objeto.
- Tornar uploads públicos “apenas por um momento”.
- Usar URLs assinadas com vida longa ou que funcionam para múltiplos usuários.
- Servir arquivos com
Content-Typeerrado, 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.
Checklist rápido e próximos passos
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.
Checklist rápido
- Valide no servidor com allowlist, checagens reais de conteúdo (não só nome de arquivo) e um tamanho máximo por arquivo.
- Armazene uploads privados por padrão e verifique permissões toda vez que o arquivo for lido, baixado ou pré-visualizado.
- Se usar uploads por URL assinada, mantenha URLs curtas, escopadas a uma chave de objeto e registre emissões para traçar abusos.
- Quarentena primeiro, varredura depois: não gere pré-visualizações nem permita downloads até o arquivo estar clean.
- Force comportamento de download seguro:
Content-Typeprevisível, nomes de arquivos seguros eattachmentpara documentos.
Próximos passos que valem a pena
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.
Perguntas frequentes
Qual é o mínimo que devo fazer para tornar uploads de arquivos “seguros o bastante"?
Comece com privado por padrão e trate todo upload como entrada não confiável. Aplique quatro controles básicos no servidor:
- Quem pode enviar
- Quais tipos de arquivo você aceita (allowlist)
- Tamanho/frequência (limites de tamanho + limites de taxa)
- Quem pode ler depois (checagens de permissão por arquivo)
Se você conseguir responder isso de forma clara, já estará à frente da maioria dos incidentes.
Por que uploads de arquivos costumam ser o primeiro incidente de segurança?
Porque usuários podem enviar uma “caixa misteriosa” que seu app armazena e depois pode servir para outros. Isso pode levar a:
- Acesso não autorizado a documentos privados
- Phishing ou takeover de contas se um arquivo for servido como conteúdo web confiável
- Quedas e contas altas por floods de uploads ou arquivos enormes
Raramente se trata apenas de “alguém enviou um vírus.”
Qual é a diferença entre armazenar arquivos e servir arquivos, e por que isso importa?
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.
Como faço para impedir que usuários baixem o arquivo enviado por outra pessoa?
Use negação por padrão e verifique acesso toda vez que um arquivo for baixado ou pré-visualizado.
Regras práticas:
- Cada registro de arquivo deve ter um proprietário (usuário/org) e um propósito (avatar, fatura, etc.)
- Na leitura/download, verifique se o requisitante tem permissão para aquele arquivo específico
- Evite segurança baseada em pastas, como “tudo em /uploads/ está ok”
- Mantenha acesso de suporte temporário e registrado (conceda acesso a um arquivo só, com expiração automática)
A maioria dos bugs reais são erros simples de “consigo ver o arquivo de outro usuário”.
Como valido o tipo do arquivo sem confiar no nome do arquivo ou no Content-Type?
Não confie na extensão do nome do arquivo nem no Content-Type enviado pelo navegador. Valide no servidor:
- Use uma allowlist de formatos por funcionalidade (ex.: JPEG/PNG para avatares, PDF para recibos)
- Detecte o tipo no servidor e verifique as magic bytes (assinatura do arquivo)
- Renomeie arquivos para armazenamento usando um ID aleatório; mantenha o nome original apenas como metadado
- Bloqueie formatos arriscados que não precisar (especialmente HTML, SVG e conteúdo semelhante a scripts)
Se os bytes não corresponderem a um formato permitido, rejeite o upload.
Que limites devo definir para prevenir ataques DoS por uploads?
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:
- Defina tamanhos máximos por funcionalidade (avatares pequenos, documentos maiores)
- Aplique limites em múltiplas camadas (app + reverse proxy + timeouts)
- Adicione limites de taxa por usuário e por IP; seja mais rígido com tráfego anônimo
Resumindo: trate cada requisição como possível abuso.
Devo usar URLs assinadas para uploads, e qual é o padrão mais seguro?
Sim, mas com cuidado. URLs assinadas permitem que o navegador envie/baixe direto do armazenamento sem tornar o bucket público.
Boas práticas:
- Mantenha URLs de escrita de curta duração (geralmente 1–5 minutos)
- Faça a URL valer para uma chave de objeto específica, não para uma pasta
- Emita URLs apenas após checagens de permissão
- Registre quem solicitou a URL e para qual arquivo
Upload direto reduz carga na API, mas exige escopo e expiração rigorosos.
Qual é um fluxo passo a passo seguro de upload que posso implementar?
O padrão mais seguro é:
- Crie um registro de upload com status
pending - Envie os bytes para um local privado
- Valide tamanho + tipo (magic bytes) no servidor
- Escaneie (geralmente de forma assíncrona) e mude o status para
cleanouquarantined - Só permita download/preview quando o status for
clean
Isso evita que arquivos “com scan falhando” ou “ainda processando” sejam compartilhados por engano.
Preciso mesmo de varredura de malware, e como é uma verificação básica?
A varredura ajuda, mas não garante 100%. Use-a como rede de segurança, não como único controle.
Abordagem prática:
- Quarentena primeiro: não exponha links até a varredura terminar
- Escaneie de forma assíncrona para escala; mostre “processando” ao usuário
- Se a varredura falhar ou expirar, trate o arquivo como suspeito e mantenha-o bloqueado
- Se permitir arquivos compactados, proteja contra zip bombs e tamanhos descomprimidos gigantes
Política é crucial: “não escaneado” nunca deve significar “disponível”.
Como devo entregar arquivos enviados de forma segura (headers, domínios, downloads)?
Sirva arquivos de forma que impessa navegadores de interpretá-los como páginas web.
Padrões úteis:
- Defina
Content-Disposition: attachmentpara documentos - Use um
Content-Typeseguro escolhido pelo servidor (ouapplication/octet-stream) - Use chaves de armazenamento opacas (não nomes de usuários) nas URLs
- Prefira um domínio de download separado para conteúdo de usuário, quando possível
Isso reduz o risco de um upload virar página de phishing ou execução de script.