Esta página describe la postura de seguridad de la plataforma tal como está implementada hoy. No mencionamos certificaciones que no tenemos: lo que sigue es la conducta real del código, no aspiraciones.

Autenticación

API key

Tu API key es una cadena con prefijo de marca (default mxcep_) seguida de 64 caracteres hexadecimales generados con random_bytes(32). En el servidor se guarda en tres formas:

  • Hash (api_key_hash): SHA-256 hexadecimal. Es lo que usamos para autenticar cada request — comparamos hash contra hash.
  • Encrypted (api_key_encrypted): la key completa, cifrada con AES-256-GCM (nonce de 12 bytes + tag de 16 bytes). Solo se desencripta cuando el dueño la solicita explícitamente desde el panel.
  • Prefix (api_key_prefix): los primeros 8 caracteres en claro. Sirve para lookup rápido sobre el set ambiguo de prefijos.

La clave de cifrado vive en una variable de entorno (API_KEY_ENCRYPTION_KEY), nunca en la base. Rotación de key: regenerar desde el panel /api. La key vieja deja de funcionar de inmediato.

Sesión web (consola)

La consola web usa JWT-en-cookie:

  • Cookie app_tokenHttpOnly, Secure (en producción), SameSite=Lax. JavaScript del navegador no puede leerla.
  • Cookie app_csrf — token CSRF de 32 bytes, legible por JS, requerido en mutaciones contra /admin y /v1/auth/*.
  • El dominio se setea desde COOKIES_DOMAIN para habilitar SSO entre subdominios (api, app, docs).

La API pública (/v1/* para clientes M2M) acepta exclusivamente API key. El JWT no se acepta en endpoints públicos.

Step-up HMAC

Las operaciones administrativas destructivas (cambios de plan, migración de suscriptores entre precios de Stripe, sync de precios) usan un flujo de dos fases:

  1. Prepare: el cliente envía la intención; el server valida y devuelve un token HMAC con el delta calculado y un nonce.
  2. Execute: el cliente envía el token de vuelta; el server verifica HMAC + nonce y aplica.

Esto previene que una sesión secuestrada accione cambios irreversibles sin confirmación explícita.

Manejo de datos sensibles

Números de cuenta (CLABE, tarjeta, celular DiMo)

Las cuentas beneficiarias se guardan en claro en user_beneficiaries.account_number y dentro del JSON request_data de cada validación. No las cifrarmos a nivel de columna — el control de acceso depende de la auth de la app y el aislamiento por user_id.

Donde las cuentas pueden aparecer en logs textuales (mensajes de error, audit log con request bodies), pasan por AuditLogger::redactPanInMessage() que enmascara cualquier corrida de 13–19 dígitos con •••• excepto los últimos 4 — para CLABEs (18 dígitos) preserva el valor por compatibilidad operativa.

Imágenes de comprobantes

Las imágenes que subes a POST /v1/validate-ocr pasan por un pipeline de sanitización antes de tocar disco:

  1. Validación de tamaño (40 bytes mínimo, 12 MB máximo).
  2. Detección de formato por magic bytes (solo JPEG/PNG/WebP).
  3. Cross-check de MIME con fileinfo.
  4. Parse estructural con getimagesizefromstring + caps de dimensión (12000 px por eje, 60 megapíxeles totales) para prevenir bombas de descompresión.
  5. Escaneo polyglot — rechaza payloads que contengan markers de PDF, ZIP, PHP, HTML, SVG, ejecutables PE/ELF, RAR o gzip.
  6. Strip de metadata: EXIF, XMP, ICC profiles, text chunks (PNG), VP8X chunks (WebP). Se eliminan GPS, modelo de dispositivo, software, fecha original.
  7. Re-validación de la salida sanitizada.

Para imágenes recuperadas vía image_url, el descargador endurece la salida HTTP: SSRF gate con resolución DNS pinneada (CURLOPT_RESOLVE), redirects limitados a 3, protocolo HTTPS-only en producción, verificación estricta de certificados (VERIFYPEER=true, VERIFYHOST=2), cap de tamaño vigilado en stream.

Audit log

Cada request a /v1/* y a la consola admin se persiste en la tabla audit_log con:

  • método, endpoint, status, IP, user-agent, request_id
  • body de la request (con secretos redactados: API keys, JWTs, Stripe keys vía SecretsRedactor::redact)
  • usuario actuante (cuando aplica)

Eventos críticos (login fallido, key regenerada, cambios de permisos, mutaciones de plan, OTPs verificados) tienen una segunda escritura en security_events, indexada por severidad y categoría. El panel /admin/security consume ambas tablas.

Para webhooks entrantes de Stripe el body original vive en webhook_ingress_events.raw_payload (no en audit_log) para evitar duplicación.

Webhooks salientes

Los eventos que mandamos a tus endpoints se firman con HMAC-SHA256 sobre el body JSON, usando un secret por endpoint que tú generas y rotas. La firma viaja en:

X-Platform-Signature: sha256=<hex>

(El nombre del header es configurable por white-label; el default es X-Platform-Signature.)

Además mandamos:

  • X-{Brand}-Event: <event_type>
  • X-{Brand}-Delivery-Id: <id>
  • X-{Brand}-Timestamp: <ISO 8601 UTC>
  • User-Agent: Platform-Webhook/1.0

Para mitigar replay attacks, verifica del lado tuyo que el timestamp esté dentro de una ventana razonable (5 minutos) además de validar la firma. Más detalles en Arquitectura de webhooks.

Retención

Los datos no viven para siempre. Estos son los timers automáticos que corren via Symfony Messenger Scheduler:

  • Comprobantes OCR (storage/comprobantes/): 30 días por defecto, configurable via COMPROBANTES_RETENTION_DAYS. Los archivos huérfanos se barren periódicamente.
  • idempotency_keys: cada hora se purgan filas expiradas (TTL por scope, 24h para validaciones).
  • otp_codes: cada hora se borran los OTPs vencidos.
  • notifications y notification_deliveries: cleanup diario con retention_days configurable.
  • telegram_link_tokens: cada hora.
  • webhook_ingress_events.raw_payload: 30 días.
  • audit_log: 1 año.
  • DLQ (dead-letter queue) de Messenger: auto-purge a los 30 días por default (DLQ_AUTO_PURGE_DAYS).
  • Health checks Banxico: 90 días.

Las validaciones (validations) y los beneficiarios (user_beneficiaries) no tienen retención automática — son tu historial y permanecen hasta que tú los borres explícitamente. El borrado soft (deleted_at) y hard están disponibles desde la consola.

Soporte de cumplimiento

Si tu equipo necesita coordinar una revisión de seguridad, un cuestionario de proveedor (vendor assessment), un acuerdo de procesamiento de datos, o una solicitud de borrado vía un usuario final, contacta al equipo de seguridad por el canal que figura en tu contrato.

No publicamos certificaciones que no auditamos. Si tu proceso requiere SOC 2, ISO 27001 u otra atestación formal, pregunta por el estado actual antes de integrar.