flawopen.com/ssrf/C-cpp

● CWE-918 · Alta
Investigación · FlawOpen

Server-Side Request Forgery in C/C++

How libcurl requests allow internal network pivots in C/C++, and how to enforce IP address validation with CURLOPT_RESOLVE.

💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina enviar a un mensajero de oficina a recoger un paquete a una tienda pública, pero dándole la dirección de la caja fuerte del despacho del director. Como el mensajero tiene pase interno, entra al despacho, abre la caja y te entrega secretos de la empresa.

Conceptos Clave y Términos

Web Application Security
Componente de arquitectura central afectado por CWE-918.
CWE-918
Clasificación estándar Common Weakness Enumeration (CWE) para ssrf-c-cpp.
Defense-in-Depth
Verificación de ingeniería multicapa y aislamiento de límites en tiempo de ejecución.

Flujo de Ataque Paso a Paso

Step 1

Ingestión de URL Remota No Confiable

La aplicación acepta una URL de usuario para descargar imágenes, webhooks o previsualizar documentos.

Step 2

Direccionamiento a Red Interna

El atacante suministra una URL apuntando a la dirección de metadatos de la nube o loopback (ej. http://169.254.169.254/latest/meta-data/).

Step 3

Salida de Socket Sin Restricciones

El cliente HTTP del servidor inicia la conexión desde la red privada interna sin validar si la IP pertenece a rangos reservados.

Step 4

Exfiltración de Credenciales de Nube

El servicio de metadados responde a la solicitud confiable entregando credenciales de IAM o tokens de seguridad.

Código Fuente: Vulnerable vs. Seguro

✕ IMPLEMENTACIÓN VULNERABLE
# VULNERABLE: Naive requests.get to user-provided URL
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url')
    # Attacker passes: http://169.254.169.254/latest/meta-data/iam/security-credentials/
    # Server queries AWS metadata and returns cloud credentials!
    resp = requests.get(url, timeout=5)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}
✓ PARCHE SEGURO Y ROBUSTO
# HARDENED: Resolve DNS, parse IP address, deny private/link-local CIDRs
import socket
import ipaddress
import urllib.parse
import requests
from flask import Flask, request, abort

app = Flask(__name__)

BLOCKED_NETWORKS = [
    ipaddress.ip_network('127.0.0.0/8'),      # Loopback
    ipaddress.ip_network('10.0.0.0/8'),       # RFC 1918 Private
    ipaddress.ip_network('172.16.0.0/12'),    # RFC 1918 Private
    ipaddress.ip_network('192.168.0.0/16'),   # RFC 1918 Private
    ipaddress.ip_network('169.254.0.0/16'),   # Link-local / Cloud IMDS
    ipaddress.ip_network('::1/128'),          # IPv6 Loopback
    ipaddress.ip_network('fc00::/7'),         # IPv6 Private ULA
]

def is_safe_ip(ip_str):
    ip = ipaddress.ip_address(ip_str)
    return not any(ip in net for net in BLOCKED_NETWORKS)

@app.route('/fetch-avatar')
def fetch_avatar():
    url = request.args.get('url', '')
    parsed = urllib.parse.urlparse(url)
    
    if parsed.scheme not in ('http', 'https') or not parsed.hostname:
        abort(400, "Invalid URL scheme")
        
    # 1. Resolve all DNS IPs for hostname
    try:
        addr_info = socket.getaddrinfo(parsed.hostname, parsed.port or (443 if parsed.scheme == 'https' else 80))
        resolved_ips = [ai[4][0] for ai in addr_info]
    except socket.gaierror:
        abort(400, "Unable to resolve host")
        
    # 2. Verify all resolved IPs are public
    for ip in resolved_ips:
        if not is_safe_ip(ip):
            abort(403, "Access to internal IP address is prohibited")
            
    # 3. Fetch with redirects disabled to prevent DNS rebinding or redirect bypass
    resp = requests.get(url, timeout=3, allow_redirects=False)
    return resp.content, 200, {'Content-Type': resp.headers.get('Content-Type', 'image/png')}

Lista de Verificación de Seguridad para Ingeniería

References