Unsafe Consumption of APIs
CWE: CWE-20 - Improper Input Validation
Problema: La aplicación confía ciegamente en APIs externas
¿Qué es?
La aplicación integra con APIs externas pero:
- No valida respuestas
- No verifica SSL/certificados
- No maneja errores adecuadamente
- No tiene timeout
- No valida origen de datos
Vectores de Ataque
1. MITM en API Externas
python
# Aplicación integra con API externa sin validar SSL
import requests
# ✗ VULNERABLE
response = requests.get(
'https://payment-api.com/verify',
verify=False # ← No valida certificado
)
# Atacante intercepta traffic
# Respuesta modificada
{
"payment_status": "verified",
"amount": 100.00
}
# Atacante cambia a:
{
"payment_status": "verified",
"amount": 0.01
}2. XXE en Respuestas XML
python
# API retorna XML
import xml.etree.ElementTree as ET
response = requests.get('https://external-api.com/data')
# Respuesta:
# <?xml version="1.0"?>
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# <data>&xxe;</data>
# ✗ VULNERABLE
root = ET.fromstring(response.content)
# Lee /etc/passwd
# ✓ SEGURO - Deshabilitar XXE
from defusedxml import ElementTree as DET
root = DET.fromstring(response.content)3. XML Bomb (Billion Laughs)
python
# API retorna XML con recursión
bomb_xml = '''<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<lolz>&lol3;</lolz>
'''
# Parser intenta expandir - consume CPU/RAM
root = ET.fromstring(bomb_xml)
# Resultado: DoS4. Inyección en Respuestas JSON
python
# Aplicación confía en JSON
response = requests.get('https://api.external.com/user/123')
data = response.json()
# ✗ VULNERABLE
user_name = data['name']
html = f"<h1>User: {user_name}</h1>"
# Si data['name'] contiene JS:
# <h1>User: <script>alert('xss')</script></h1>
# XSS en la aplicación
# ✓ SEGURO - Escapar HTML
from markupsafe import escape
user_name = escape(data['name'])5. Redirection Chains
python
# Aplicación sigue redirects sin validar
response = requests.get('https://api.external.com/endpoint')
# ¿Redirect a?
# 1. Mismo dominio - OK
# 2. Trusted partner - OK
# 3. file:///etc/passwd - ✗ PELIGROSO
# 4. http://internal-server - ✗ PELIGROSO
# 5. 127.0.0.1:8080 - ✗ PELIGROSO
# ✓ SEGURO - Validar redirects
response = requests.get(
'https://api.external.com/endpoint',
allow_redirects=False # O validar manualmente
)6. Inyección de Dependencias vía API
python
# Aplicación integra con API de plugins
response = requests.get('https://plugin-api.com/get-plugins')
plugins = response.json()
for plugin in plugins:
# ✗ VULNERABLE - Ejecutar código de API
exec(plugin['code'])
# Atacante compromete plugin-api.com
# Inyecta código maliciosoExplotación Práctica
Caso 1: MITM en Pago
python
# Aplicación verifica pago llamando API
def verify_payment(transaction_id):
response = requests.get(
f'https://payment-gateway.com/verify/{transaction_id}',
verify=False # ← VULNERABLE
)
if response.json()['status'] == 'approved':
complete_order()
return True
return False
# Atacante (MITM):
# 1. Intercepta solicitud
# 2. Cambia respuesta a {"status": "approved"}
# 3. Orden se completa sin pagarCaso 2: XXE → RCE
python
# API de terceros retorna XML vulnerable a XXE
def get_user_data(user_id):
response = requests.get(
f'https://crm-api.com/users/{user_id}',
timeout=5
)
# Parse XML sin protección
root = ET.fromstring(response.content)
return root.find('name').text
# Payload XXE:
# <?xml version="1.0"?>
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/shadow">]>
# <user><name>&xxe;</name></user>
# Resultado: /etc/shadow leídoMitigación
1. Validar Certificados SSL
python
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context
# ✗ NUNCA
response = requests.get(url, verify=False)
# ✓ SIEMPRE verificar
response = requests.get(url, verify=True)
# ✓ CON certificado personalizado
response = requests.get(
url,
verify='/path/to/ca-bundle.crt',
cert=('/path/to/client.crt', '/path/to/client.key')
)2. Deshabilitar XXE
python
from defusedxml import ElementTree as DET
from defusedxml.minidom import parseString
# ✗ VULNERABLE
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
# ✓ SEGURO
root = DET.fromstring(response.content)
# O manualmente deshabilitar
parser = ET.XMLParser()
parser.parser.SetParamEntityParsing(ET.XML_PARAM_ENTITY_PARSING_NEVER)
root = ET.fromstring(response.content, parser=parser)3. Validar Redirects
python
import requests
from urllib.parse import urlparse
ALLOWED_DOMAINS = [
'api.app.com',
'partner1.com',
'partner2.com'
]
def safe_request(url):
"""
Request con validación de redirects
"""
response = requests.get(
url,
allow_redirects=False # No seguir automáticamente
)
# Si hay redirect, validar
if response.status_code in [301, 302, 307, 308]:
redirect_url = response.headers.get('Location')
parsed = urlparse(redirect_url)
# Whitelist de dominios
if parsed.netloc not in ALLOWED_DOMAINS:
raise Exception(f"Redirect to untrusted domain: {parsed.netloc}")
# Seguir redirect manualmente
return requests.get(redirect_url)
return response
# Usar
response = safe_request('https://api.partner.com/endpoint')4. Input Validation y Sanitization
python
from markupsafe import escape
import html
import json
def process_external_api_response(data):
"""
Validar y sanitizar respuesta de API
"""
# Si es JSON string, parsear
if isinstance(data, str):
data = json.loads(data)
# Validar estructura
if not isinstance(data, dict):
raise ValueError("Invalid response format")
# Validar y escapar cada campo
result = {}
for key, value in data.items():
# Validar tipo
if not isinstance(value, (str, int, float, bool)):
continue # Ignorar tipos inesperados
# Escapar strings
if isinstance(value, str):
result[key] = escape(value)
else:
result[key] = value
return result
# Uso
response = requests.get('https://external-api.com/data')
safe_data = process_external_api_response(response.json())5. Timeout y Límite de Tamaño
python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def requests_with_retry(max_retries=3, timeout=5, max_size=1024*1024):
"""
Requests con timeout, retry y límite de tamaño
"""
session = requests.Session()
# Timeout global
session.timeout = timeout
# Retry strategy
retry = Retry(
total=max_retries,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
# Wrapper para limitar tamaño
original_request = session.request
def request_with_size_limit(*args, **kwargs):
kwargs['stream'] = True
response = original_request(*args, **kwargs)
# Verificar tamaño
size = int(response.headers.get('content-length', 0))
if size > max_size:
raise Exception(f"Response too large: {size} bytes")
return response
session.request = request_with_size_limit
return session
# Usar
session = requests_with_retry(timeout=5, max_size=10*1024*1024)
response = session.get('https://external-api.com/data')6. Implementar Circuit Breaker
python
from datetime import datetime, timedelta
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=60):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failures = 0
self.last_failure = None
self.state = 'closed' # closed, open, half_open
def call(self, func, *args, **kwargs):
if self.state == 'open':
if datetime.now() - self.last_failure > timedelta(seconds=self.timeout):
self.state = 'half_open'
else:
raise Exception("Circuit is open")
try:
result = func(*args, **kwargs)
self.failures = 0
self.state = 'closed'
return result
except Exception as e:
self.failures += 1
self.last_failure = datetime.now()
if self.failures >= self.failure_threshold:
self.state = 'open'
raise
# Usar
breaker = CircuitBreaker(failure_threshold=5)
def call_external_api():
return requests.get('https://external-api.com/data')
try:
response = breaker.call(call_external_api)
except Exception as e:
print(f"API error: {e}")Checklist
- [ ] Siempre verificar certificados SSL (verify=True)
- [ ] Usar defusedxml para parsear XML
- [ ] Deshabilitar XXE
- [ ] Validar y sanitizar todas las respuestas
- [ ] Timeout en todas las requests (máx 5-10 segundos)
- [ ] Límite de tamaño de respuesta
- [ ] Validar redirects (whitelist de dominios)
- [ ] Circuit breaker para APIs externas
- [ ] Logging de errores de integración
- [ ] Monitoreo de latencia de APIs externas
- [ ] Retry logic con backoff exponencial