Boas práticas de segurança em uploads e processamento de arquivos

A funcionalidade de upload de arquivos é uma das portas de entrada mais exploradas por atacantes em aplicações web. Um sistema mal configurado pode permitir desde a execução remota de código até ataques de negação de serviço. Este artigo apresenta as principais práticas para garantir a segurança no upload e processamento de arquivos, abordando desde a validação inicial até a entrega segura ao cliente.

1. Validação rigorosa do tipo e conteúdo do arquivo

A primeira linha de defesa começa antes mesmo de o arquivo ser salvo no servidor. A validação deve ocorrer exclusivamente no lado servidor, jamais confiando em verificações feitas pelo cliente.

Verificação de extensão com whitelist

Utilize listas de permissões (whitelist) em vez de listas de bloqueio (blacklist). Enquanto uma blacklist tenta prever todas as extensões maliciosas, uma whitelist define exatamente o que é aceito.

# Exemplo de whitelist de extensões
EXTENSOES_PERMITIDAS = {'.jpg', '.jpeg', '.png', '.gif', '.pdf', '.docx'}

def validar_extensao(nome_arquivo):
    extensao = os.path.splitext(nome_arquivo)[1].lower()
    if extensao not in EXTENSOES_PERMITIDAS:
        raise ValueError("Tipo de arquivo não permitido")

Análise de magic bytes e MIME

A extensão do arquivo pode ser facilmente falsificada. Verifique os magic bytes (assinatura de arquivo) e o cabeçalho MIME real:

# Verificação de magic bytes para JPEG
def verificar_magic_bytes(caminho_arquivo):
    with open(caminho_arquivo, 'rb') as f:
        cabecalho = f.read(4)
    # JPEG inicia com FF D8 FF
    if cabecalho[:3] != b'\xff\xd8\xff':
        raise ValueError("Arquivo não é um JPEG válido")

Validação de tamanho e integridade

Defina limites máximos de tamanho e implemente verificação de integridade:

TAMANHO_MAXIMO = 10 * 1024 * 1024  # 10 MB

def validar_tamanho(arquivo):
    if arquivo.tamanho > TAMANHO_MAXIMO:
        raise ValueError("Arquivo excede o tamanho máximo permitido")

2. Armazenamento seguro e isolamento de arquivos

O local onde os arquivos são armazenados é tão crítico quanto as validações iniciais.

Diretórios fora da raiz pública

Nunca salve arquivos dentro do diretório público da aplicação web. Crie diretórios isolados com permissões restritas:

DIRETORIO_UPLOADS = "/var/data/uploads/"  # Fora do document root do servidor web

Nomes únicos e aleatórios

Substitua o nome original do arquivo por identificadores únicos para evitar conflitos e ataques de path traversal:

import uuid
import os

def gerar_nome_seguro(extensao):
    nome_unico = str(uuid.uuid4())
    return f"{nome_unico}{extensao}"

caminho_final = os.path.join(DIRETORIO_UPLOADS, gerar_nome_seguro('.jpg'))

Permissões restritas

Configure permissões mínimas necessárias no sistema de arquivos:

# Permissões: apenas o proprietário pode ler/escrever
os.chmod(caminho_final, 0o600)

3. Sanitização contra injeção e execução maliciosa

Arquivos aparentemente inofensivos podem conter código malicioso oculto.

Remoção de metadados executáveis

Imagens podem conter scripts em metadados EXIF. Utilize bibliotecas especializadas para limpeza:

# Exemplo com Pillow (Python)
from PIL import Image

def sanitizar_imagem(caminho_origem, caminho_destino):
    imagem = Image.open(caminho_origem)
    # Salva sem metadados EXIF
    imagem.save(caminho_destino, format='JPEG', exif=b'')

Escaneamento com antivírus

Para ambientes críticos, integre um scanner de malware:

# Exemplo com ClamAV
import subprocess

def escanear_arquivo(caminho):
    resultado = subprocess.run(['clamscan', '--no-summary', caminho], 
                              capture_output=True, text=True)
    if 'FOUND' in resultado.stdout:
        raise ValueError("Arquivo infectado detectado")

Prevenção de path traversal

Valide caminhos absolutos e relativos para evitar que o usuário acesse diretórios não autorizados:

def validar_caminho_seguro(caminho_base, nome_arquivo):
    caminho_completo = os.path.normpath(os.path.join(caminho_base, nome_arquivo))
    if not caminho_completo.startswith(caminho_base):
        raise ValueError("Tentativa de path traversal detectada")
    return caminho_completo

4. Processamento seguro de arquivos no servidor

O processamento de arquivos (conversão, redimensionamento) deve ser feito com ferramentas seguras e atualizadas.

Uso de bibliotecas confiáveis

Prefira bibliotecas mantidas ativamente e com histórico de correções de segurança:

# Processamento de PDF com PyMuPDF (fitz)
import fitz

def processar_pdf_seguro(caminho_origem, caminho_destino):
    doc = fitz.open(caminho_origem)
    # Remove JavaScript e ações incorporadas
    for pagina in doc:
        pagina.clean_contents()
    doc.save(caminho_destino)
    doc.close()

Limitação de recursos

Evite ataques DoS limitando memória e tempo de CPU:

import resource
import signal

def processar_com_limites(caminho_arquivo):
    # Limita tempo de CPU para 30 segundos
    signal.alarm(30)
    # Limita memória para 256 MB
    resource.setrlimit(resource.RLIMIT_AS, (256*1024*1024, 256*1024*1024))
    # Processamento do arquivo...

Recompressão de arquivos

Converter e recompactar arquivos pode remover conteúdo malicioso oculto:

# Recompressão de imagem PNG para JPEG
from PIL import Image

def reconverter_imagem(caminho_origem, caminho_destino):
    img = Image.open(caminho_origem).convert('RGB')
    img.save(caminho_destino, 'JPEG', quality=85)

5. Controle de acesso e autenticação no upload

Todo upload deve estar vinculado a um usuário autenticado e autorizado.

Autenticação forte e validação de permissões

def verificar_permissao_upload(usuario, tipo_arquivo):
    if not usuario.esta_autenticado():
        raise PermissionError("Usuário não autenticado")
    if tipo_arquivo not in usuario.tipos_permitidos:
        raise PermissionError("Tipo de arquivo não autorizado para este usuário")

Rate limiting por usuário e IP

Implemente limites de requisições para evitar abusos:

# Exemplo com Redis para rate limiting
import redis
import time

cliente_redis = redis.Redis()

def verificar_rate_limit(usuario_id, ip):
    chave = f"upload:{usuario_id}:{ip}"
    tentativas = cliente_redis.incr(chave)
    if tentativas == 1:
        cliente_redis.expire(chave, 60)  # Expira em 60 segundos
    if tentativas > 10:
        raise Exception("Limite de uploads excedido. Tente novamente mais tarde.")

Logs detalhados

Registre todas as operações para auditoria:

import logging

logger = logging.getLogger('upload_seguranca')

def registrar_upload(usuario_id, nome_arquivo, tamanho, sucesso):
    logger.info(f"Upload: usuario={usuario_id}, arquivo={nome_arquivo}, "
                f"tamanho={tamanho}, sucesso={sucesso}")

6. Proteção contra ataques de negação de serviço (DoS)

Uploads maliciosos podem sobrecarregar o servidor. Implemente proteções em múltiplas camadas.

Limitação por sessão e intervalo

TOTAL_MAXIMO_POR_SESSAO = 100 * 1024 * 1024  # 100 MB
INTERVALO_RESET = 3600  # 1 hora

def verificar_cota_sessao(sessao_id, tamanho_arquivo):
    total_atual = cache.get(f"cota:{sessao_id}", 0)
    if total_atual + tamanho_arquivo > TOTAL_MAXIMO_POR_SESSAO:
        raise Exception("Cota de upload excedida para esta sessão")
    cache.incrby(f"cota:{sessao_id}", tamanho_arquivo)
    cache.expire(f"cota:{sessao_id}", INTERVALO_RESET)

Filas de processamento assíncrono

Evite processar arquivos grandes na mesma thread da requisição HTTP:

# Exemplo com Celery
from celery import Celery

app = Celery('processamento', broker='redis://localhost:6379')

@app.task
def processar_arquivo_assincrono(caminho_arquivo, usuario_id):
    try:
        sanitizar_imagem(caminho_arquivo, caminho_final)
        registrar_upload(usuario_id, caminho_final, True)
    except Exception as e:
        registrar_upload(usuario_id, caminho_arquivo, False, str(e))

Timeouts e limites de concorrência

Configure o servidor web para limitar conexões simultâneas e tempo de processamento:

# Configuração Nginx para limitar uploads
client_max_body_size 10M;
client_body_timeout 30s;
proxy_read_timeout 60s;
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;

7. Entrega segura de arquivos ao cliente

A entrega de arquivos também deve ser protegida para evitar acesso não autorizado.

Proxy intermediário para downloads

Nunca sirva arquivos diretamente do diretório de uploads. Utilize um script intermediário que valide permissões:

def servir_arquivo(arquivo_id, usuario):
    caminho_real = obter_caminho_por_id(arquivo_id)
    if not usuario.tem_acesso(arquivo_id):
        raise PermissionError("Acesso negado ao arquivo")

    response = FileResponse(caminho_real, as_attachment=True)
    response['Content-Disposition'] = f'attachment; filename="{arquivo_id}.jpg"'
    return response

Cabeçalhos HTTP corretos

Configure cabeçalhos para evitar execução de conteúdo malicioso no navegador:

# Headers de segurança para download
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="arquivo_seguro.pdf"
X-Content-Type-Options: nosniff
Cache-Control: no-store

Implemente tokens de acesso temporários para downloads:

import hmac
import hashlib
import time

def gerar_link_temporario(arquivo_id, usuario_id, expiracao=3600):
    timestamp = int(time.time()) + expiracao
    mensagem = f"{arquivo_id}:{usuario_id}:{timestamp}"
    token = hmac.new(SECRET_KEY.encode(), mensagem.encode(), hashlib.sha256).hexdigest()
    return f"/download/{arquivo_id}?token={token}&exp={timestamp}"

def validar_link_temporario(arquivo_id, token, timestamp, usuario_id):
    if int(time.time()) > int(timestamp):
        return False
    mensagem = f"{arquivo_id}:{usuario_id}:{timestamp}"
    token_esperado = hmac.new(SECRET_KEY.encode(), mensagem.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(token, token_esperado)

Conclusão

A segurança em uploads e processamento de arquivos exige uma abordagem em camadas, combinando validação rigorosa, armazenamento isolado, sanitização de conteúdo, controle de acesso e proteção contra DoS. Cada camada funciona como uma barreira adicional que dificulta a exploração de vulnerabilidades.

Implementar todas essas práticas reduz significativamente o risco de ataques como execução remota de código, path traversal, upload de malware e negação de serviço. A segurança nunca é um estado final, mas um processo contínuo de atualização e monitoramento.

Referências