Saltar a contenido

Threat model — conexión Shopify ↔ TimeLiber

Estado: diseño previo a implementación
Fecha: 2026-08-31
Arquitectura: ADR 0002

Activos protegidos

  • identidad y sesión del usuario TimeLiber;
  • pertenencia y rol dentro de organization_id;
  • identidad canónica Shopify (shop_id, *.myshopify.com);
  • access/refresh tokens y client secret Shopify;
  • scopes concedidos;
  • pedidos, productos, inventario y datos protegidos;
  • receipts de webhooks, cursores y estado de sincronización;
  • relación única tienda Shopify ↔ organización TimeLiber.

Fronteras de confianza

Navegador TimeLiber
  └─ HTTPS ─ Commerce web/API ─ PostgreSQL/Redis
                    │ HTTPS interno autenticado
Shopify Admin ─ App Bridge ─ TimeLiber Connect
     ├─ GraphQL Admin API
     └─ webhooks HTTPS firmados ─ Commerce webhook ingress

El navegador, query strings, redirects, headers reenviables y payloads webhook son entradas no confiables. La red Docker no sustituye autenticación de servicio. PostgreSQL/RLS es una frontera adicional, no la primera validación.

Invariantes

  1. Ningún shop_domain proporcionado por navegador decide el tenant.
  2. El tenant sólo proviene de una sesión TimeLiber válida y membresía comprobada.
  3. La tienda sólo proviene de una identidad autenticada por Shopify.
  4. Vincular exige demostrar ambas identidades dentro de una ventana corta y en una operación atómica.
  5. Una tienda activa no puede pertenecer a dos organizaciones.
  6. Tokens nunca llegan al navegador, logs, eventos internos ni respuestas públicas.
  7. Todo registro comercial contiene organization_id, filtro explícito y RLS.
  8. Un webhook se autentica antes de buscar tenant o producir efectos.
  9. Aceptar un webhook y procesarlo son operaciones separadas y auditables.
  10. Desinstalar revoca capacidad de sincronizar inmediatamente, aunque el borrado regulatorio ocurra después.

Amenazas y controles

Amenaza Escenario Controles obligatorios Prueba requerida
Tenant hijacking Usuario vincula una tienda a empresa ajena sesión TimeLiber, capability CONNECT_CHANNELS, intent ligado a tenant/user, consumo atómico miembro/no-miembro y carrera de intents
Shop spoofing Cliente envía otro shop_domain shop canónico obtenido server-side de Shopify; normalización estricta; shop_id como identidad primaria dominio alterado/replay
Confused deputy TimeLiber Connect usa su autoridad para vincular sin consentimiento TimeLiber intent de un uso, TTL corto, audience/purpose explícitos, confirmación visible intent ausente, vencido, usado o de otro propósito
CSRF/login CSRF atacante fuerza callback/link mecanismos oficiales Shopify, state/nonce donde aplique, SameSite apropiado, no depender de cookies third-party callback sin estado y sesión cruzada
Token disclosure token aparece en UI, log o excepción transporte server-to-server TLS, redaction estructurada, cifrado antes de commit, respuestas allowlist captura de logs y errores inducidos
Token replay credencial retirada sigue en uso tokens offline expirables, refresh con lock/compare-and-swap, versión de credencial, revocación en uninstall refresh concurrente y token retirado
Scope escalation app gana permisos no aprobados por TimeLiber allowlist de scopes, comparar granted vs requested, app/scopes_update, estado degraded scope extra/faltante
Webhook forgery POST falso crea pedido/acción HMAC sobre bytes crudos, comparación constant-time, límites de body/topic/shop firma alterada y encoding distinto
Webhook replay/duplicate Shopify o atacante reenvía delivery unique provider + webhook ID, receipt antes del 2xx, handlers idempotentes mismo ID simultáneo
Event loss proceso cae después del 2xx commit de inbox antes de responder, cola durable, leases, retry/backoff y DLQ kill entre receipt y job
Cross-tenant worker job procesa con contexto anterior payload incluye tenant; nueva transacción; SET LOCAL ROLE y GUC por job; limpiar contexto jobs alternados A/B
SSRF shop/domain controla URL saliente no almacenar URL arbitraria; construir endpoint sólo desde dominio Shopify canónico validado dominios IP, puertos, suffix tricks
Over-fetch de PII query/webhook obtiene campos innecesarios selección GraphQL explícita, include_fields/filtros cuando aplique, gate PCD snapshot de query/payload
Uninstall race jobs siguen tras desinstalación transición atómica a uninstalled, revocar/invalidar token, worker verifica estado antes de llamada uninstall con jobs en cola
Key compromise clave cifra todos los tokens envelope encryption, key version, rotación, acceso mínimo, procedimiento de revocación rotación y decrypt de versiones
Operator abuse personal consulta PII RBAC, audit log, acceso just-in-time, exportaciones controladas auditoría de cada acceso

Lifecycle seguro

Crear intención

  • Usuario TimeLiber autenticado y organización activa.
  • Capability CONNECT_CHANNELS comprobada.
  • Se genera secreto aleatorio de alta entropía; PostgreSQL conserva sólo hash.
  • TTL máximo recomendado: 10 minutos.
  • Estado inicial pending; un intent anterior puede invalidarse.
  • La respuesta contiene una ruta Shopify-owned o instrucciones de enlace, nunca credenciales.

Vincular instalación

  • TimeLiber Connect autentica al usuario/shop mediante flujo oficial.
  • Su backend llama al contrato interno de Commerce; no lo hace el navegador.
  • Commerce bloquea el intent (SELECT … FOR UPDATE), comprueba hash/TTL/purpose/tenant/user y unicidad global de shop_id.
  • Cifra tokens antes de persistir y consume el intent en la misma transacción.
  • Si falla cualquier paso, no queda conexión parcialmente activa.

El mecanismo visual exacto para transportar el intent —deep link aprobado o código de un uso— queda pendiente de auditar el template y probarlo dentro de Shopify Admin. No se asumirá que cookies o parámetros sobreviven al cambio de contexto.

Refresh

  • Un lock distribuido por instalación impide refresh simultáneo.
  • Compare-and-swap sobre versión/token evita que una respuesta tardía restaure una credencial retirada.
  • Se conserva sólo la credencial vigente y metadata de rotación, no tokens históricos.
  • Un fallo definitivo mueve la conexión a reauthorization_required; no se reintenta infinitamente.

Webhook

  • Leer bytes crudos con tamaño máximo.
  • Verificar HMAC antes de parsear/procesar.
  • Extraer headers allowlist y resolver la instalación por shop_id/dominio canónico.
  • Guardar receipt idempotente y responder 2xx.
  • Procesar en worker con contexto tenant nuevo y reconciliation GraphQL cuando el orden de eventos sea ambiguo.

Uninstall y borrado

  • app/uninstalled: marcar conexión uninstalled, impedir jobs nuevos, invalidar credenciales y cancelar leases pendientes.
  • customers/data_request: registrar solicitud y producir respuesta auditable al merchant.
  • customers/redact: eliminar/anonimizar datos correspondientes dentro del plazo aplicable.
  • shop/redact: borrar datos de la tienda conforme a la política y registrar sólo evidencia no personal permitida.

Datos prohibidos en observabilidad

  • access/refresh tokens, client secret, authorization code e intent secret;
  • HMAC completo o headers de autorización;
  • payload webhook completo;
  • email, teléfono, nombre y dirección;
  • query GraphQL con variables que contengan PII.

Logs permitidos: IDs internos, organization_id, shop ID seudonimizado, topic, webhook ID, resultado, latencia, intento, API version, error code sanitizado y correlation ID.

Gates de seguridad

  • revisión del código real de TimeLiber Connect;
  • prueba E2E de install/link/replay/reinstall/uninstall;
  • prueba PostgreSQL de RLS sobre todas las tablas nuevas;
  • fuzz de dominio Shopify y HMAC;
  • test concurrente de intent y token refresh;
  • inspección automática de logs para secretos/PII;
  • threat model actualizado antes de Protected Customer Data nivel 2.