El panel de facturación de OpenAI mostraba cargos que el fundador no podía explicar. La aplicación tenía aproximadamente 800 usuarios en un modelo freemium, pero el uso de la API tendía a los $2,000 al mes, mucho más alto de lo que justificaba la actividad de los usuarios en la plataforma. El fundador asumió que tenían un prompt ineficiente en algún lugar y tomó nota para investigar.
Siete minutos después de iniciar un escaneo de Penetrify, la razón se hizo evidente: la clave de la API de OpenAI estaba siendo devuelta a los usuarios en los encabezados de respuesta HTTP de cada llamada a la API proxy. 800 usuarios la habían visto. Algunos de ellos la estaban usando.
La Arquitectura: De Dónde Provino la Fuga
La aplicación era un backend de FastAPI que servía un frontend de React. Su funcionalidad principal era actuar como proxy para las solicitudes de los usuarios a la API de OpenAI, añadiendo prompts de sistema personalizados, almacenando el historial de conversaciones y aplicando la capa de ingeniería de prompts propietaria del fundador. Este es un patrón común para los productos de envoltura de IA: el valor no es el modelo, es el producto construido a su alrededor.
Así funcionaba la aplicación:
- El usuario envía un prompt desde el frontend de React
- El frontend lo envía a
POST /api/generate - El manejador de FastAPI añade el prompt del sistema y llama a la API de OpenAI
- FastAPI devuelve la finalización al frontend
En algún lugar de la implementación de la ruta de FastAPI, el encabezado Authorization de la solicitud saliente de OpenAI —que contenía la clave de la API en formato de token Bearer— estaba siendo reenviado en la respuesta. Esta es una clase específica de error de reenvío de encabezados: la aplicación estaba pasando los encabezados de respuesta de la llamada a la API de OpenAI ascendente en lugar de construir sus propios encabezados de respuesta.
Los encabezados de respuesta en cada llamada a /api/generate incluían:
HTTP/1.1 200 OK
Content-Type: application/json
Authorization: Bearer sk-proj-...[OpenAI API key]
...
{"completion": "..."}
Cada usuario que había utilizado la función de generación —los 800— había recibido la clave de la API en los encabezados de respuesta de sus solicitudes. Era visible en la pestaña Red de las DevTools del navegador, en cualquier proxy HTTP y en cualquier cliente programático que leyera los encabezados de respuesta.
Qué te Ofrece una Clave de la API de OpenAI
Una clave de la API de OpenAI sin restricciones de uso otorga al titular acceso completo a la cuota de la API de la cuenta correspondiente. Esto significa:
- Acceso ilimitado a modelos a expensas del propietario de la clave — GPT-4o, o1, o3, generación de imágenes, embeddings, fine-tuning
- Sin límite por solicitud hasta que se alcance el límite de gasto mensual de la cuenta
- Acceso a cualquier modelo fine-tuned que la cuenta haya creado
- Capacidad para leer archivos almacenados si la cuenta utiliza la API de Archivos
Para un fundador individual cuya aplicación procesa $200–400/mes en uso legítimo, que su clave sea utilizada indebidamente externamente puede elevar la factura mensual a $2,000, $5,000 o más, dependiendo de cuán ampliamente circule la clave y qué estén generando los abusadores.
El modelo de costos para el abuso de la API de OpenAI es asimétrico: el atacante no paga nada, el propietario de la clave paga por todo.
Los Picos de Facturación Inexplicables, Explicados
Una vez identificada la exposición de la clave, los picos de facturación tuvieron sentido. El fundador consultó el panel de uso de OpenAI filtrado por endpoint y tiempo. El patrón de picos mostró solicitudes de alto volumen que no se correlacionaban con la actividad de los usuarios en la plataforma: solicitudes a las 3 de la mañana, solicitudes de rangos de IP que no coincidían con ninguna geografía de usuario conocida, solicitudes de tipos de modelos que la aplicación no utilizaba.
Alguien había extraído la clave — posiblemente varias personas — y la estaba utilizando directamente contra la API de OpenAI, saltándose la aplicación por completo. Las solicitudes iban directamente a OpenAI utilizando las credenciales extraídas, no a través de la aplicación del fundador.
La clave había estado expuesta desde aproximadamente la primera semana del lanzamiento público de la aplicación. En el momento del escaneo, había estado activa y filtrándose durante varios meses.
Otros Hallazgos
La exposición de la clave de OpenAI fue el hallazgo más inmediatamente perjudicial, pero se informaron tres problemas adicionales:
MEDIO — IDOR en /api/history/:userId
La aplicación almacenaba el historial de conversaciones por usuario y lo exponía en un endpoint predecible:
GET /api/history/abc123
El manejador de ruta obtenía el historial de conversaciones para el ID de usuario en el parámetro de ruta sin verificar si el usuario solicitante era propietario de esos registros. Cualquier usuario autenticado podía leer el historial de conversaciones de cualquier otro usuario sustituyendo su ID. Dado que las conversaciones incluían prompts proporcionados por el usuario, esto también era una exposición de privacidad: un atacante podía leer qué preguntas otros usuarios habían estado haciendo a la herramienta de IA.
MEDIO — Modo de depuración de FastAPI habilitado en producción
La aplicación se estaba ejecutando con FastAPI(debug=True). En modo de depuración, cualquier excepción no manejada devuelve un rastreo de pila completo en la respuesta HTTP, incluyendo rutas de archivos internas, versiones de dependencias y nombres de variables de entorno (aunque no sus valores). Esta información es directamente útil para planificar ataques adicionales — conocer la versión exacta de FastAPI, la versión de Pydantic y la versión de Python reduce significativamente la lista de CVEs aplicables.
El modo de depuración también habilita la documentación interactiva de FastAPI en /docs y /redoc por defecto, la cual era accesible en producción y documentaba cada endpoint interno de la API, incluyendo aquellos no destinados al acceso de usuarios.
BAJO — HTTP no redirigiendo a HTTPS
La versión HTTP de la aplicación servía contenido completo sin redirigir a HTTPS. En redes públicas o compartidas, un atacante realizando un ataque man-in-the-middle podría interceptar sesiones no cifradas y extraer tokens de sesión, prompts enviados por el usuario y respuestas de la API.
La Solución: Implementada la Misma Noche
El fundador implementó soluciones para todos los hallazgos dentro de las tres horas de recibir el informe.
Rotar la clave primero
Antes de tocar cualquier código, la acción inmediata fue revocar la clave comprometida en el panel de control de OpenAI y generar una nueva. Esto detuvo instantáneamente cualquier abuso en curso. La rotación de claves de OpenAI es inmediata — la clave antigua deja de funcionar en el momento en que la eliminas.
Corregir el error de reenvío de encabezados
La causa raíz fue que la ruta de FastAPI estaba utilizando un cliente HTTP genérico que reenviaba todos los encabezados de respuesta de la llamada upstream de OpenAI. La solución fue construir encabezados de respuesta explícitos en lugar de pasar los de upstream:
# Antes (vulnerable) — reenviando todos los encabezados upstream
upstream_response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
json=payload
)
return Response(
content=upstream_response.content,
headers=dict(upstream_response.headers) # ← esto reenvía el encabezado Authorization de vuelta
)
# Después (corregido) — construcción explícita de la respuesta
upstream_response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.OPENAI_API_KEY}", ...},
json=payload
)
completion_data = upstream_response.json()
return JSONResponse(content={"completion": completion_data["choices"][0]["message"]["content"]})
# Solo los datos que queremos devolver explícitamente — no se reenvían encabezados upstream
Corregir el IDOR
El endpoint del historial de conversaciones se actualizó para extraer el ID de usuario del JWT verificado en lugar de hacerlo del parámetro de ruta:
@router.get("/api/history")
async def get_history(current_user: User = Depends(get_current_user)):
# El ID de usuario proviene del JWT verificado — no puede ser suplantado
history = await db.get_history(user_id=current_user.id)
return history
Desactivar el modo de depuración
# En config.py
app = FastAPI(
debug=settings.DEBUG, # lee de la variable de entorno
docs_url=None if not settings.DEBUG else "/docs", # oculta la documentación en producción
redoc_url=None if not settings.DEBUG else "/redoc"
)
Con DEBUG=false configurado en el entorno de producción, la documentación interactiva y las respuestas de error detalladas desaparecieron inmediatamente en el siguiente despliegue.
Añadir límites de uso de OpenAI como red de seguridad
Además de solucionar la fuga, el fundador añadió dos medidas defensivas para limitar el radio de impacto de cualquier futura exposición de claves:
Límites de uso: En el panel de control de OpenAI, bajo Facturación → Límites de uso, establece un límite máximo mensual y un umbral de notificación suave. Incluso si una clave se ve comprometida de nuevo, la capacidad del atacante para acumular cargos está limitada.
Claves dedicadas por servicio: Crea una clave API separada para cada aplicación o entorno. Si una clave se ve comprometida, puedes rotar solo esa clave sin interrumpir otros servicios, y los registros de uso de cada clave están claramente separados, lo que facilita mucho la detección de accesos no autorizados.
¿Qué tan común es esto?
La exposición de claves API en respuestas HTTP es menos común que la exposición en paquetes JavaScript, pero la vemos regularmente específicamente en aplicaciones envoltorio de IA. El patrón casi siempre tiene la misma causa raíz: un desarrollador que construye una capa de proxy utiliza un cliente HTTP genérico que reenvía los encabezados de respuesta, y no audita lo que contienen esos encabezados.
El error de reenvío de encabezados es fácil de cometer porque a menudo simplifica la implementación. ¿Por qué construir una nueva respuesta cuando puedes reenviar la de origen? La respuesta, en este caso, es que la respuesta de origen contiene credenciales que no quieres compartir con tus usuarios.
Si tu aplicación actúa como proxy para llamadas a OpenAI, Anthropic o cualquier otra API externa, audita explícitamente tus encabezados de respuesta. Utiliza una herramienta como curl -v o las Herramientas de desarrollo de tu navegador para examinar cada encabezado devuelto por cada endpoint de la API. Los encabezados son fáciles de pasar por alto precisamente porque la mayoría de las veces son poco interesantes, lo que los convierte en un escondite tan eficaz para una fuga.
El contexto de la aplicación YC
El fundador estaba preparando una solicitud de YC en el momento del escaneo. La combinación de picos de facturación inexplicables, una clave API expuesta y una vulnerabilidad IDOR que afectaba al historial de conversaciones de todos los usuarios habría sido un problema significativo para explicar a los inversores —o, peor aún, para descubrir después de la financiación.
Los problemas de seguridad en la etapa de prelanzamiento o de tracción inicial son solucionables en horas. Los mismos problemas descubiertos después de un incidente de seguridad, una notificación de violación de datos o una historia mediática hostil tardan meses en recuperarse y pueden acabar con una empresa que aún no ha construido la buena voluntad para sobrevivir al ciclo de noticias.
El fundador ejecutó Penetrify de nuevo antes de enviar la solicitud de YC. El informe resultó limpio.
