Seguridad · Sharp Edges cultivapay-python-sdk v2.1 2026-06-18

Análisis de APIs Peligrosas & Footguns

CultivaPay SDK — Auditoría pre-lanzamiento · sdk/auth.py · sdk/tokens.py · sdk/config.py

3
Críticos
2
Altos
1
Medios
1
Bajos
7 archivos
Surface analizada
Metodología aplicada
1
Identificación de Superficie
Mapeo de APIs de autenticación, criptografía, tokens JWT y configuración de entorno.
2
Sondeo de Casos Límite
Pruebas con valores 0, null, string vacío, negativos y algoritmos prohibidos.
3
Modelado de Amenazas
3 adversarios: El Tramposo (config maliciosa), El Vago (copy-paste), El Confundido (tipos).
4
Validación de Hallazgos
Código mínimo de explotación y mitigación concreta para cada footgun encontrado.
Resumen de hallazgos
ID Severidad Archivo Categoría Descripción
CP-001 Crítico sdk/auth.py Silent Bypass Sin API key → verify_webhook() retorna True siempre
CP-002 Crítico sdk/tokens.py Alg. Confusion decode_payment_token acepta alg "none" → bypass de firma JWT
CP-003 Crítico sdk/config.py String Truthy auth_bypass como string "false" → is_auth_bypassed() siempre True
CP-004 Alto sdk/auth.py Timing Attack Comparación de firmas con == permite timing oracle
CP-005 Alto sdk/auth.py Algo Selection hash_algo acepta md5/sha1; sin validación en constructor
CP-006 Medio sdk/config.py Zero Timeout timeout=0 elimina límite de conexión silenciosamente
CP-007 Bajo sdk/tokens.py Mutable Default metadata={} como default mutable — contaminación entre llamadas
Hallazgos detallados
Crítico
CP-001 · Silent Bypass
Verificación de Webhook Omitida sin API Key
sdk/auth.py verify_webhook()
Si api_key es una cadena vacía o no se proporciona, la función devuelve True incondicionalmente. Esto significa que cualquier webhook falso — incluyendo los de un atacante — será aceptado como legítimo. El comentario "modo dev" racionaliza el footgun pero no lo excusa: los secrets de desarrollo llegan a producción con regularidad.
Código vulnerable
PELIGROSO
def verify_webhook(self, payload, signature, timestamp):
    if not self.api_key:
        return True   # ← FOOTGUN: skip silencioso en dev (llega a prod)
    computed = hmac.new(self.api_key.encode(), payload,
                        self.hash_algo).hexdigest()
    if computed == signature:   # ← timing attack además
        return True
    return False
Escenario de explotación
😈
El Vago: Configura CULTIVAPAY_KEY="" en .env de staging, lo sube al repositorio. El pipeline de CI/CD lo promueve a producción. Cualquier POST al endpoint de webhook es aceptado.
🕵️
El Tramposo: Llama directamente al endpoint de webhook con datos fabricados (ej. {"event":"payment.success","amount":10000}). Cobra 10.000€ que nunca pagó.
Mitigación
SEGURO
def verify_webhook(self, payload, signature, timestamp):
    if not self.api_key:
        raise ValueError("api_key requerida para verificar webhooks")
    # Comparación constant-time
    computed = hmac.new(self.api_key.encode(), payload, "sha256").hexdigest()
    return hmac.compare_digest(computed, signature)
Principio de diseño aplicado

El camino fácil debe ser el seguro. Lanzar una excepción cuando falta la clave obliga al desarrollador a configurarla correctamente. "Sin clave = modo dev permisivo" es una racionalización que debe rechazarse.

Crítico
CP-002 · Algorithm Confusion (JWT)
decode_payment_token Acepta algoritmo "none"
sdk/tokens.py decode_payment_token()
La lista algorithms=["HS256", "RS256", "none"] es el footgun JWT canónico: un atacante puede forjar tokens con {"alg":"none"} sin necesitar ninguna clave. Adicionalmente, generate_payment_token puede generar tokens sin firma cuando key=None.
Explotación (3 líneas)
EXPLOIT
import base64, json

# Forjar token sin clave, con amount=99999
header = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b'=')
payload = base64.urlsafe_b64encode(b'{"amount":99999,"currency":"EUR"}').rstrip(b'=')
forged = header + b'.' + payload + b'.'   # sin firma

# ← CultivaPay lo acepta porque "none" está en algorithms
sdk.decode_payment_token(forged)
Peligroso
def decode_payment_token(
  token, key=None,
  algorithms=["HS256","RS256","none"]
):
  return jwt.decode(
    token, key, algorithms=algorithms
  )
Seguro
def decode_payment_token(token, key):
  _ALLOWED_ALGOS = {"HS256"}
  return jwt.decode(
    token, key,
    algorithms=list(_ALLOWED_ALGOS),
    options={"verify_exp": True}
  )
Crítico
CP-003 · String Truthy Footgun
auth_bypass Siempre Truthy — "false" es True en Python
sdk/config.py is_auth_bypassed()
os.getenv("CULTIVAPAY_AUTH_BYPASS", "false") devuelve un string. En Python, cualquier string no vacío — incluyendo "false" — es truthy. Por tanto is_auth_bypassed() retorna siempre True, desactivando la autenticación en toda instancia del SDK.
PELIGROSO
# .env de producción intenta deshabilitar bypass:
CULTIVAPAY_AUTH_BYPASS=false

# Pero en Python:
>>> bool("false")
True   # ← "false" como string ES truthy. Auth bypasseado en prod.

def is_auth_bypassed():
    return CULTIVAPAY_CONFIG["auth_bypass"]  # devuelve string, no bool
SEGURO
CULTIVAPAY_CONFIG = {
    ...
    # Parsear explícitamente; solo "true" habilita
    "auth_bypass": os.getenv("CULTIVAPAY_AUTH_BYPASS", "").lower() == "true",
}

# O mejor aún: eliminar la opción. No debe existir un bypass de auth en prod.
# Si se necesita testing, usar un flag de test explícito con check de entorno.
Alto
CP-004 · Timing Oracle
Comparación de HMAC con == Permite Timing Attack
sdk/auth.py verify_webhook()
La comparación computed == signature usa comparación de strings que cortocircuita al primer carácter diferente. Un atacante puede medir tiempos de respuesta para determinar byte a byte la firma correcta, forjando webhooks sin conocer la clave. Requiere miles de peticiones pero es automatizable.
⏱️
El Tramposo: Envía webhooks con firmas que comparten 1, 2, 3... caracteres del prefijo real. Mide latencia de respuesta (en ms). Caracteriza la firma completa en ~256 × 64 = 16.384 peticiones promedio. Factible en red local o CDN compartida.
Inseguro
# Cortocircuita en primer ≠
if computed == signature:
    return True
Constant-time
# Compara todos los bytes siempre
return hmac.compare_digest(
    computed, signature
)
Alto
CP-005 · Algorithm Selection Footgun
hash_algo Acepta MD5/SHA1 sin Validación
sdk/auth.py __init__() → hash_algo
El parámetro hash_algo del constructor acepta cualquier string que hashlib reconozca: md5, sha1, sha256. MD5 está roto como función criptográfica; SHA1 está depreciado para HMAC en contextos de pago. Un desarrollador que lea "sha256 por defecto" puede no ver el peligro de cambiarlo.
PELIGROSO
# Legítimo según la API, pero inseguro:
auth = CultivaPayAuth(api_key=key, hash_algo="md5")

# MD5 HMAC tiene colisiones conocidas. Un atacante con webhooks legítimos
# puede construir colisiones para forjar firmas.
SEGURO — Rechazar en construcción
_ALLOWED_HASH_ALGOS = {"sha256", "sha384", "sha512"}

def __init__(self, api_key, hash_algo="sha256", ...):
    if hash_algo not in _ALLOWED_HASH_ALGOS:
        raise ValueError(
            f"hash_algo '{hash_algo}' no permitido. "
            f"Usar uno de: {_ALLOWED_HASH_ALGOS}"
        )
    self.hash_algo = hash_algo
Medio
CP-006 · Zero-Value Timeout Cliff
timeout=0 Elimina Límite de Conexión Silenciosamente
sdk/config.py CULTIVAPAY_TIMEOUT
int(os.getenv("CULTIVAPAY_TIMEOUT", "0")) hace que el default sea 0, lo que en la mayoría de clientes HTTP (requests, httpx) significa sin timeout. Una respuesta lenta del servidor CultivaPay o un man-in-the-middle que congele conexiones causará que los workers del cliente cuelguen indefinidamente, provocando un DoS de los procesos de la agencia.
SEGURO
# Default explícito y no-zero; validar en lectura:
_DEFAULT_TIMEOUT = 30  # segundos
timeout_raw = int(os.getenv("CULTIVAPAY_TIMEOUT", str(_DEFAULT_TIMEOUT)))
if timeout_raw <= 0:
    raise ValueError("CULTIVAPAY_TIMEOUT debe ser > 0 segundos")
Bajo
CP-007 · Mutable Default Argument
metadata={} Como Argumento Por Defecto Mutable
sdk/tokens.py generate_payment_token()
En Python, los argumentos por defecto mutables se comparten entre todas las llamadas a la función. Si algún código modifica metadata internamente, los datos de un pago contaminarán los tokens de pagos posteriores. Raramente es explotable directamente pero puede filtrar datos de clientes entre transacciones.
Peligroso
def generate_payment_token(
  amount, currency,
  metadata={}   # shared!
):
Seguro
def generate_payment_token(
  amount, currency,
  metadata=None
):
  metadata = metadata or {}
Principios del "Pit of Success"