Upload de arquivos: validação, restrições e armazenamento seguro

1. Introdução aos Riscos de Upload de Arquivos

1.1. Por que upload de arquivos é um vetor crítico de ataque

Funcionalidades de upload de arquivos estão presentes em praticamente toda aplicação web moderna — perfis de usuário, sistemas de anexos, galerias de imagens, portais de documentos. Essa superfície de ataque é particularmente perigosa porque permite que o atacante insira conteúdo arbitrário no servidor. Um único arquivo malicioso pode comprometer toda a infraestrutura.

1.2. Principais ameaças

As três ameaças mais comuns associadas ao upload de arquivos são:

  • Execução remota de código (RCE): arquivos como PHP, ASP, JSP ou scripts shell podem ser interpretados pelo servidor web se armazenados em diretórios acessíveis.
  • Distribuição de malware: o servidor se torna um hospedeiro de arquivos maliciosos que infectam outros usuários.
  • Negação de serviço (DoS): upload de arquivos muito grandes, em grande quantidade ou com conteúdo compressível (zip bombs) pode exaurir recursos do servidor.

1.3. Visão geral das camadas de defesa

A segurança no upload de arquivos deve ser implementada em três camadas:

  1. Entrada: validação rigorosa antes de qualquer processamento.
  2. Processamento: sanitização, verificação de conteúdo e transformação segura.
  3. Saída: armazenamento isolado e distribuição controlada.

2. Validação Rigorosa do Lado do Servidor

2.1. Verificação de tipo MIME real

Nunca confie na extensão do arquivo ou no cabeçalho Content-Type enviado pelo cliente. Ambos podem ser facilmente falsificados. Utilize bibliotecas que leem os primeiros bytes do arquivo (magic bytes) para determinar o tipo real.

# Exemplo em Python usando python-magic
import magic

def verificar_tipo_real(caminho_arquivo):
    mime = magic.from_file(caminho_arquivo, mime=True)
    tipos_permitidos = ['image/jpeg', 'image/png', 'application/pdf']
    if mime not in tipos_permitidos:
        raise ValueError(f"Tipo {mime} não permitido")
    return mime

2.2. Validação de extensão contra lista branca

Sempre utilize uma whitelist de extensões explicitamente permitidas. Nunca use blacklists, pois são fáceis de contornar com extensões duplas ou variações.

# Validação de extensão com whitelist
import os

EXTENSOES_PERMITIDAS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}

def validar_extensao(nome_arquivo):
    _, extensao = os.path.splitext(nome_arquivo.lower())
    if extensao not in EXTENSOES_PERMITIDAS:
        raise ValueError(f"Extensão {extensao} não permitida")

2.3. Limitação de tamanho e verificação de integridade

Defina limites máximos de tamanho e verifique se o arquivo não foi corrompido ou truncado durante o upload.

# Validação de tamanho máximo (5 MB)
TAMANHO_MAXIMO = 5 * 1024 * 1024  # 5 MB

def validar_tamanho(arquivo):
    arquivo.seek(0, os.SEEK_END)
    tamanho = arquivo.tell()
    arquivo.seek(0)
    if tamanho > TAMANHO_MAXIMO:
        raise ValueError(f"Arquivo excede {TAMANHO_MAXIMO} bytes")
    if tamanho == 0:
        raise ValueError("Arquivo vazio")

3. Sanitização e Processamento Seguro do Conteúdo

3.1. Reprocessamento de imagens

Imagens podem conter metadados maliciosos, scripts em comentários EXIF ou payloads embutidos em pixels. A melhor prática é reprocessar toda imagem recebida: redimensionar, recompressar e salvar em formato padronizado.

# Reprocessamento de imagem com Pillow
from PIL import Image

def sanitizar_imagem(caminho_entrada, caminho_saida):
    imagem = Image.open(caminho_entrada)
    # Redimensionar para tamanho máximo seguro
    imagem.thumbnail((1200, 1200))
    # Salvar em formato padronizado, sem metadados
    imagem.save(caminho_saida, 'JPEG', quality=85, optimize=True)

3.2. Análise com antivírus

Integre um scanner antivírus para detectar malware antes de armazenar o arquivo. Soluções como ClamAV podem ser acionadas via linha de comando.

# Verificação com ClamAV
import subprocess

def verificar_antivirus(caminho_arquivo):
    resultado = subprocess.run(
        ['clamscan', '--stdout', caminho_arquivo],
        capture_output=True, text=True
    )
    if 'FOUND' in resultado.stdout:
        raise ValueError("Malware detectado no arquivo")

3.3. Rejeição de poliglotas e scripts embutidos

Arquivos poliglotas combinam múltiplos formatos (ex.: GIF que também é PHP). Utilize verificações de integridade estrutural do formato declarado.


4. Restrições de Nomenclatura e Metadados

4.1. Geração de nomes únicos

Nunca utilize o nome original do arquivo para armazenamento. Gere nomes aleatórios com UUID ou hash do conteúdo.

import uuid
import hashlib

def gerar_nome_seguro(arquivo):
    # Usar hash do conteúdo para evitar duplicatas
    hash_conteudo = hashlib.sha256(arquivo.read()).hexdigest()
    arquivo.seek(0)
    return f"{hash_conteudo}_{uuid.uuid4().hex}"

4.2. Sanitização contra path traversal

Mesmo que o nome original não seja usado para armazenamento, sanitize-o para logging e exibição segura.

import re

def sanitizar_nome_original(nome):
    # Remove caracteres de path traversal e não alfanuméricos
    nome_limpo = re.sub(r'[^\w\.\-]', '_', nome)
    return nome_limpo[:100]  # Limita tamanho

4.3. Remoção de metadados

Metadados EXIF podem conter geolocalização, dados do dispositivo e strings injetadas. Remova-os completamente.


5. Armazenamento Seguro dos Arquivos

5.1. Armazenamento fora da raiz web

Nunca armazene uploads em diretórios servidos diretamente pelo servidor web (ex.: public/uploads). Use diretórios fora da raiz ou serviços de armazenamento externos.

# Estrutura de diretórios segura
/projeto
  /app
    /uploads          # Fora da raiz web
  /public
    index.php         # Raiz web

5.2. Permissões restritivas e isolamento

Configure permissões mínimas: o diretório de uploads deve ter permissão de escrita apenas para o processo da aplicação, e nunca permissão de execução.

# Configuração de permissões no Linux
chmod 750 /projeto/app/uploads
chown www-data:www-data /projeto/app/uploads

5.3. Servir arquivos via proxy de download

Nunca forneça URLs diretas para os arquivos. Crie um script intermediário que verifica autenticação, autorização e aplica cabeçalhos de segurança.

# Exemplo de endpoint de download proxy (Flask)
@app.route('/download/<file_id>')
def download_arquivo(file_id):
    if not usuario_autorizado():
        abort(403)
    caminho = obter_caminho_seguro(file_id)
    return send_file(
        caminho,
        as_attachment=True,
        download_name='documento.pdf'
    )

6. Controle de Acesso e Autenticação

6.1. Autenticação obrigatória

Todo upload deve exigir autenticação. Verifique tokens de sessão, JWT ou chaves de API antes de processar qualquer arquivo.

6.2. Rate limiting

Implemente limitação de taxa para evitar abuso: número máximo de uploads por minuto/hora por usuário ou IP.

# Configuração de rate limiting (Flask-Limiter)
limiter = Limiter(key_func=get_remote_address)
limiter.limit("10 per minute")(upload_route)

6.3. Políticas de expiração

Arquivos temporários ou não confirmados devem ser removidos automaticamente após período definido (ex.: 24 horas).


7. Monitoramento e Resposta a Incidentes

7.1. Logging detalhado

Registre todos os eventos de upload: timestamp, ID do usuário, nome original, tipo detectado, tamanho, hash do conteúdo.

import logging

def log_upload(usuario, nome_original, tipo_real, tamanho, hash):
    logging.info(
        f"Upload: user={usuario}, file={nome_original}, "
        f"type={tipo_real}, size={tamanho}, hash={hash}"
    )

7.2. Alertas para padrões anômalos

Configure alertas para atividades suspeitas: múltiplos uploads do mesmo tipo incomum, uploads em horários atípicos, tentativas de upload de arquivos bloqueados.

7.3. Procedimentos de quarentena

Arquivos suspeitos (detectados por antivírus ou heurísticas) devem ser movidos para uma área de quarentena isolada e notificados à equipe de segurança.


Referências