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
- Ningún
shop_domainproporcionado por navegador decide el tenant. - El tenant sólo proviene de una sesión TimeLiber válida y membresía comprobada.
- La tienda sólo proviene de una identidad autenticada por Shopify.
- Vincular exige demostrar ambas identidades dentro de una ventana corta y en una operación atómica.
- Una tienda activa no puede pertenecer a dos organizaciones.
- Tokens nunca llegan al navegador, logs, eventos internos ni respuestas públicas.
- Todo registro comercial contiene
organization_id, filtro explícito y RLS. - Un webhook se autentica antes de buscar tenant o producir efectos.
- Aceptar un webhook y procesarlo son operaciones separadas y auditables.
- 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_CHANNELScomprobada. - 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 deshop_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ónuninstalled, 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.