V4 · API y Servicios Web#
¿De qué trata esta sección?#
Las APIs modernas — REST, GraphQL, WebSocket — exponen funcionalidad directamente a clientes que no son navegadores: apps móviles, scripts automatizados, otros servicios. Esto elimina muchas de las protecciones que el navegador ofrece automáticamente, y exige que el desarrollador sea explícito sobre cómo se estructura, valida y protege cada mensaje HTTP.
Los ataques a APIs no siempre buscan robar datos. A veces buscan colapsar el servicio, confundir al proxy o balanceador para que interprete mal los mensajes, o extraer el esquema interno de la aplicación para planificar ataques más sofisticados.
**Importante:** Esta sección **no reemplaza** a las otras. Los requerimientos de autenticación (V7), manejo de sesiones (V4) y validación de entrada (V1, V2) también aplican a las APIs. V4 solo cubre los aspectos específicos del protocolo HTTP y los formatos de API.
V4.1 · Seguridad General de Servicios Web#
Esta sección cubre prácticas básicas de higiene en servicios web: cabeceras de contenido correctas, manejo de redirecciones, métodos HTTP permitidos y firma de mensajes.
| # | Requisito | Nivel |
|---|---|---|
| 4.1.1 | Verificar que toda respuesta HTTP con cuerpo incluye un header Content-Type que corresponde al contenido real, incluyendo el parámetro charset (ej: application/json; charset=utf-8). | 1 |
| 4.1.2 | Solo los endpoints de cara al usuario (acceso por navegador) deben redirigir automáticamente de HTTP a HTTPS. Los demás endpoints no deben redirigir silenciosamente para que las filtraciones de datos en HTTP no pasen desapercibidas. | 2 |
| 4.1.3 | Verificar que los headers establecidos por capas intermedias (load balancers, proxies, BFF) como X-Real-IP o X-Forwarded-For no pueden ser sobreescritos por el usuario final. | 2 |
| 4.1.4 | Verificar que solo los métodos HTTP explícitamente soportados por la API pueden usarse. Los métodos no soportados deben ser bloqueados. | 3 |
| 4.1.5 | Para solicitudes altamente sensibles que atraviesan múltiples sistemas, usar firmas digitales por mensaje (además del TLS) para garantizar integridad end-to-end. | 3 |
Ejemplo: Content-Type correcto en respuestas de API#
from flask import Flask, jsonify, make_response
app = Flask(__name__)
# ❌ MAL: responder JSON sin declarar el Content-Type correcto
@app.route('/api/usuario')
def usuario_mal():
return '{"id": 1, "nombre": "Ana"}'
# El navegador puede interpretar esto como texto plano y abrirlo directamente,
# lo que en ciertos contextos puede llevar a XSS
# ✅ BIEN: Content-Type explícito con charset
@app.route('/api/usuario')
def usuario_bien():
data = {"id": 1, "nombre": "Ana"}
response = make_response(jsonify(data))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response
# Flask's jsonify ya pone application/json, pero verificar el charset
# es especialmente importante para APIs que retornan texto con caracteres especialesEjemplo: Bloquear métodos HTTP no permitidos#
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# ✅ Definir explícitamente qué métodos acepta cada endpoint
@app.route('/api/usuarios', methods=['GET', 'POST'])
def usuarios():
if request.method == 'GET':
return jsonify(obtener_usuarios())
elif request.method == 'POST':
return jsonify(crear_usuario(request.json)), 201
# Flask devolverá 405 Method Not Allowed automáticamente para otros métodos.
# Para APIs más estrictas, se puede agregar un handler global:
@app.errorhandler(405)
def metodo_no_permitido(e):
return jsonify({
'error': 'Método no permitido',
'metodos_permitidos': e.valid_methods
}), 405Ejemplo: Proteger headers de capa intermedia#
# Escenario: el app confía en X-Real-IP para auditoría o rate limiting.
# Un atacante podría enviar su propia cabecera X-Real-IP para falsificar su IP.
# ❌ MAL: confiar directamente en el header enviado por el cliente
@app.route('/api/accion')
def accion_mal():
ip_cliente = request.headers.get('X-Real-IP', request.remote_addr)
registrar_accion(ip_cliente) # El atacante puede falsificar su IP
# ✅ BIEN: el proxy/load balancer debe sobrescribir este header,
# y la app debe rechazar el header si viene directamente del cliente.
# En Nginx:
# proxy_set_header X-Real-IP $remote_addr; ← Nginx sobrescribe con la IP real
# En Flask, verificar que el request viene de un proxy confiable:
PROXIES_CONFIABLES = {'10.0.0.1', '10.0.0.2'} # IPs de los load balancers
@app.route('/api/accion')
def accion_bien():
if request.remote_addr not in PROXIES_CONFIABLES:
# Request directo al app — no usar headers de proxy
ip_real = request.remote_addr
else:
# Request de un proxy confiable — usar el header
ip_real = request.headers.get('X-Real-IP', request.remote_addr)
registrar_accion(ip_real)V4.2 · Validación de Estructura de Mensajes HTTP#
Esta sección previene ataques que explotan ambigüedades en cómo distintos componentes (proxies, balanceadores, servidores) interpretan los límites de un mensaje HTTP. Son ataques de contrabando de peticiones (request smuggling) e inyección de cabeceras.
| # | Requisito | Nivel |
|---|---|---|
| 4.2.1 | Verificar que todos los componentes (load balancers, firewalls, servidores) determinan los límites de mensajes HTTP usando el mecanismo correcto según la versión. En HTTP/1.x: si Transfer-Encoding está presente, ignorar Content-Length. En HTTP/2 y HTTP/3: verificar que Content-Length coincide con los DATA frames. | 2 |
| 4.2.2 | Al generar respuestas HTTP, el header Content-Length no debe contradecir la longitud real del contenido, para evitar request smuggling. | 3 |
| 4.2.3 | La aplicación no debe enviar ni aceptar mensajes HTTP/2 o HTTP/3 con headers de conexión específica como Transfer-Encoding, para prevenir response splitting e inyección de headers. | 3 |
| 4.2.4 | La aplicación solo debe aceptar requests HTTP/2 y HTTP/3 donde los headers no contengan secuencias \r, \n o \r\n, para prevenir header injection. | 3 |
| 4.2.5 | Si la app construye URIs o headers de requests (como Authorization o Cookie), debe validar que no son demasiado largos para el componente receptor, evitando denegación de servicio. | 3 |
Ejemplo: ¿Qué es request smuggling y por qué ocurre?#
# Un ataque de request smuggling ocurre cuando el frontend (proxy) y el
# backend (app) interpretan diferente dónde termina un request HTTP.
# Ejemplo con HTTP/1.1 malicioso (conceptual):
# El atacante envía un request con AMBOS headers: Content-Length y Transfer-Encoding.
POST /api/datos HTTP/1.1
Host: app.ejemplo.com
Content-Length: 13
Transfer-Encoding: chunked
0
GET /admin HTTP/1.1 ← Este segundo request queda "oculto" dentro del primero
Host: app.ejemplo.com
# Si el proxy usa Content-Length y el backend usa Transfer-Encoding,
# el backend ve dos requests: el original y el "/admin" inyectado.
# Mitigación: asegurarse de que proxy y backend usen el mismo mecanismo.
# Preferir HTTP/2 que elimina esta ambigüedad por diseño.Ejemplo: Sanitizar headers en HTTP/2#
import re
# HTTP/2 no permite \r o \n en valores de headers
# Si construyes headers dinámicamente, valida antes de enviarlos
def sanitizar_valor_header(valor: str) -> str:
"""Eliminar caracteres de control que podrían inyectar headers."""
# Rechazar si contiene CR, LF o CRLF
if re.search(r'[\r\n]', valor):
raise ValueError(f"Valor de header inválido: contiene caracteres de control")
return valor
# Ejemplo de uso al construir un header de respuesta dinámico:
@app.route('/api/exportar')
def exportar():
nombre_archivo = request.args.get('nombre', 'export')
# ❌ MAL: usar el input del usuario directamente en un header
# response.headers['Content-Disposition'] = f'attachment; filename={nombre_archivo}'
# Un atacante podría enviar nombre_archivo = "x\r\nX-Injected: evil"
# ✅ BIEN: sanitizar antes de usar en header
nombre_seguro = sanitizar_valor_header(nombre_archivo)
# Además, solo permitir caracteres alfanuméricos en el nombre
nombre_seguro = re.sub(r'[^a-zA-Z0-9_\-]', '_', nombre_seguro)
response = make_response(generar_csv())
response.headers['Content-Disposition'] = f'attachment; filename="{nombre_seguro}.csv"'
return responseV4.3 · GraphQL#
GraphQL permite a los clientes pedir exactamente los datos que necesitan, pero esta flexibilidad puede ser explotada para lanzar ataques de denegación de servicio mediante consultas extremadamente complejas o anidadas.
| # | Requisito | Nivel |
|---|---|---|
| 4.3.1 | Usar allowlist de queries, límites de profundidad, límites de cantidad, o análisis de costo para prevenir DoS mediante queries costosas o profundamente anidadas. | 2 |
| 4.3.2 | Deshabilitar la introspección de GraphQL en producción, a menos que la API sea pública para terceros. | 2 |
Ejemplo: Por qué GraphQL puede ser abusado#
# Un atacante puede enviar una query profundamente anidada para agotar recursos:
# (query "bomb" — exponencial en tiempo de procesamiento)
{
usuarios {
amigos {
amigos {
amigos {
amigos {
amigos {
nombre # 6 niveles de profundidad = miles de resolvers ejecutados
}
}
}
}
}
}
}Ejemplo: Límites de profundidad y complejidad con graphene + Python#
# Usando la librería graphene con validaciones de seguridad
from graphene import ObjectType, String, List, Schema
from graphql import validate, parse
from graphql.validation import NoSchemaIntrospectionCustomRule
# --- Límite de profundidad ---
def calcular_profundidad(node, profundidad=0):
"""Calcular la profundidad máxima de una query GraphQL."""
max_profundidad = profundidad
for campo in getattr(node, 'selection_set', None) and \
node.selection_set.selections or []:
max_profundidad = max(
max_profundidad,
calcular_profundidad(campo, profundidad + 1)
)
return max_profundidad
MAX_PROFUNDIDAD = 5
MAX_COMPLEJIDAD = 100
@app.route('/graphql', methods=['POST'])
def graphql_endpoint():
body = request.get_json()
query_str = body.get('query', '')
# Parsear sin ejecutar
try:
documento = parse(query_str)
except Exception:
return jsonify({'error': 'Query inválida'}), 400
# Verificar profundidad
for definicion in documento.definitions:
profundidad = calcular_profundidad(definicion)
if profundidad > MAX_PROFUNDIDAD:
return jsonify({
'error': f'Query demasiado profunda: {profundidad} niveles (máx {MAX_PROFUNDIDAD})'
}), 400
# Ejecutar solo si pasa los controles
resultado = schema.execute(query_str)
return jsonify(resultado.data)
# --- Deshabilitar introspección en producción ---
import os
if os.environ.get('FLASK_ENV') == 'production':
# Agregar regla de validación que bloquea queries de introspección
extra_validaciones = [NoSchemaIntrospectionCustomRule]
else:
extra_validaciones = []
# Esto evita que atacantes mapeen todo el esquema de datos de la app
# ejecutando queries como: { __schema { types { name fields { name } } } }V4.4 · WebSocket#
WebSocket establece un canal de comunicación bidireccional y persistente. Por su naturaleza distinta a HTTP, las protecciones habituales de sesión y origen no aplican automáticamente — deben implementarse explícitamente.
| # | Requisito | Nivel |
|---|---|---|
| 4.4.1 | Verificar que todas las conexiones WebSocket usan WebSocket sobre TLS (WSS). | 1 |
| 4.4.2 | Durante el handshake inicial HTTP de WebSocket, verificar el header Origin contra una lista de orígenes permitidos. | 2 |
| 4.4.3 | Si el manejo de sesión estándar de la app no puede usarse, usar tokens dedicados que cumplan los requerimientos de seguridad de gestión de sesión. | 2 |
| 4.4.4 | Los tokens dedicados de sesión WebSocket deben obtenerse o validarse a través de la sesión HTTPS autenticada antes de escalar a WebSocket. | 2 |
Ejemplo: WebSocket seguro con Flask-SocketIO#
from flask import Flask, request, session
from flask_socketio import SocketIO, disconnect, emit
app = Flask(__name__)
app.secret_key = 'clave_secreta_segura'
# ✅ Usar solo WSS (WebSocket Secure) — configurar en el servidor web (Nginx/etc.)
# En producción: el servidor Nginx maneja el TLS, Flask recibe la conexión internamente
socketio = SocketIO(app, cors_allowed_origins=[
'https://app.midominio.com' # ← Allowlist de orígenes: solo nuestra app
])
# ❌ NUNCA usar: cors_allowed_origins='*' para WebSockets con sesiones
@socketio.on('connect')
def manejar_conexion():
# Validar que el usuario tiene una sesión HTTP activa ANTES de aceptar WebSocket
usuario_id = session.get('usuario_id')
if not usuario_id:
# Rechazar la conexión WebSocket si no hay sesión HTTP autenticada
disconnect()
return False
# Generar token de sesión dedicado para esta conexión WebSocket
token_ws = generar_token_websocket(usuario_id)
session['ws_token'] = token_ws
emit('autenticado', {'token': token_ws})
print(f"WebSocket aceptado para usuario {usuario_id}")
@socketio.on('accion_sensible')
def manejar_accion(data):
# Re-validar en cada mensaje sensible, no solo en la conexión
token_recibido = data.get('ws_token')
token_esperado = session.get('ws_token')
if not token_recibido or token_recibido != token_esperado:
emit('error', {'mensaje': 'Token de sesión inválido'})
disconnect()
return
# Procesar acción solo si el token es válido
procesar_accion(data)Ejemplo: ¿Por qué verificar el Origin en WebSocket?#
# A diferencia de fetch() y XMLHttpRequest, el navegador no aplica
# CORS automáticamente a WebSocket. Cualquier sitio puede iniciar
# una conexión WebSocket a tu servidor si no verificas el Origin.
# Sin verificación, un sitio malicioso puede hacer:
# new WebSocket('wss://tu-api.com/ws')
# ...y si el usuario tiene una cookie de sesión, la conexión se autentica.
# Con Flask-SocketIO, el parámetro cors_allowed_origins maneja esto.
# Para implementación manual:
from flask import request
@app.route('/ws-upgrade')
def websocket_upgrade():
origen = request.headers.get('Origin', '')
origenes_permitidos = {'https://app.midominio.com', 'https://m.midominio.com'}
if origen not in origenes_permitidos:
return 'Origen no permitido', 403
# Continuar con el upgrade a WebSocket...Resumen: ¿Qué cubre cada subsección?#
| Sección | Pregunta que responde | Técnica principal |
|---|---|---|
| V4.1 | ¿Está bien configurado el servicio básico? | Content-Type correcto, métodos permitidos, headers de proxy |
| V4.2 | ¿Interpretan igual todos los componentes el mensaje HTTP? | Validación de Content-Length vs Transfer-Encoding, sanitización de headers |
| V4.3 | ¿Puede un atacante colapsar la API con una sola query? | Límites de profundidad/complejidad, introspección deshabilitada |
| V4.4 | ¿Es seguro el canal WebSocket? | WSS, validación de Origin, tokens de sesión dedicados |
**Regla de oro de V4:** Las APIs son la superficie de ataque más directa de tu aplicación. No asumas que quien llama a tu API es un navegador bien portado — valida estructura, origen y método en cada request, sin excepción.