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.

#RequisitoNivel
4.1.1Verificar 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.2Solo 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.3Verificar 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.4Verificar 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.5Para 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 especiales

Ejemplo: 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
    }), 405

Ejemplo: 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.

#RequisitoNivel
4.2.1Verificar 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.2Al generar respuestas HTTP, el header Content-Length no debe contradecir la longitud real del contenido, para evitar request smuggling.3
4.2.3La 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.4La 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.5Si 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 response

V4.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.

#RequisitoNivel
4.3.1Usar 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.2Deshabilitar 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.

#RequisitoNivel
4.4.1Verificar que todas las conexiones WebSocket usan WebSocket sobre TLS (WSS).1
4.4.2Durante el handshake inicial HTTP de WebSocket, verificar el header Origin contra una lista de orígenes permitidos.2
4.4.3Si 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.4Los 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ónPregunta que respondeTé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.