Resumen
En events.wordpress.org (la plataforma de eventos y grupos de WordPress, parte del código "Meta" de wordcamp.org), los eventos se gestionan con el plugin GatherPress. Los eventos recurrentes pasan además por un mu-plugin propio, gatherpress-recurring-events (gpre), que publica su propia API REST bajo gpre/v1.
GatherPress permite proteger un evento con contraseña de entrada de WordPress. Desde la versión 0.35.1, su API oficial no entrega la lista de confirmaciones de asistencia (RSVP) de un evento protegido hasta que quien la pide ha introducido la contraseña. Esa comprobación la hace Event::can_read_rsvps(), que exige que post_password_required( $post ) sea falso.
El fallo estaba en la ruta equivalente del envoltorio gpre:
GET /wp-json/gpre/v1/event/{recurrence_id}/rsvp-responses?post_id={id}
Esa ruta se registraba con permission_callback => '__return_true', es decir, sin ningún control de acceso. La función que responde (responses(), en mu-plugins/gatherpress-recurring-events/includes/class-rest-api.php) solo comprobaba que la entrada estuviera publicada y que la ocurrencia existiera, y luego devolvía directamente ( new Rsvp( $post_id ) )->responses(). Nunca llamaba a Event::can_read_rsvps() ni a post_password_required().
Resultado: la API oficial respondía 401 a una petición anónima sobre un evento protegido, mientras que la ruta de gpre devolvía la misma lista de asistentes sin pedir nada. El investigador aclara que en los eventos abiertos que la lista sea pública es intencionado; el problema es que la contraseña se podía saltar. Otras partes del mismo código Meta, como el bloque de contenido de la portada de Grupos, sí respetan post_password_required.
El programa calificó el reporte de severidad media, lo dio por resuelto y pagó recompensa (importe no público). Se envió el 14 de septiembre de 2026 y se divulgó el 5 de noviembre de 2026.
Pasos de reproducción
El investigador lo reprodujo en un entorno local (el Docker de Meta), no en producción. Hace falta WordPress con GatherPress 0.35.1 o posterior y el mu-plugin gatherpress-recurring-events de wordcamp.org Meta, con un evento con RSVP activado.
- Crear un
gatherpress_event, publicarlo y ponerle contraseña de entrada (por ejemplosecret-pass). - Configurarlo como serie recurrente con los metadatos de gpre (semanal u otra frecuencia), para que se generen ocurrencias y exista un
recurrence_id(por ejemplo20260921T100000). - Con un usuario normal con sesión iniciada, confirmar asistencia a esa ocurrencia, de modo que gpre asocie el comentario RSVP a ella.
- Cerrar sesión (o usar un navegador limpio, sin cookies) y no introducir la contraseña del evento.
- Pedir la lista a la API oficial de GatherPress. La respuesta es
401 rest_forbidden, que es lo correcto:
GET /wp-json/gatherpress/v1/event/rsvp-responses?post_id=<EVENT_ID>
- Pedir lo mismo a la ruta de gpre, con el mismo evento y su
recurrence_id:
GET /wp-json/gpre/v1/event/<RECURRENCE_ID>/rsvp-responses?post_id=<EVENT_ID>
La respuesta es un 200 con la lista de asistentes:
{
"success": true,
"data": {
"attending": {
"count": 1,
"records": [
{
"name": "rsvper",
"user_id": 4,
"profile": "http://…/author/rsvper/",
"comment_id": 2
}
]
}
}
}
- Comprobar que en la petición no iba ninguna cookie
wp-postpass_*ni cabeceraAuthorization.
Impacto
Cualquiera, sin cuenta y sin conocer la contraseña, podía leer quién iba a asistir a un evento recurrente protegido de la plataforma de eventos y grupos de WordPress: nombre, ID de usuario, URL del perfil, ID del comentario y estado y fechas de la confirmación. Son justo los datos que el organizador quería dejar tras la contraseña. No hacía falta ningún rol especial.
Remediación
El reporte figura como resuelto, pero no detalla el cambio aplicado. Lo que proponía el investigador:
- Usar en la
permission_callbackdegpre/v1/.../rsvp-responsesla misma comprobación que GatherPress, por ejemploEvent::can_read_rsvps( (int) $request['post_id'] ), como hace la ruta oficial concan_read_event_rsvps. - Repetir la comprobación al principio de
responses()como defensa en profundidad. - Añadir un test de PHPUnit: evento recurrente publicado con contraseña y petición anónima a gpre → 401/403; con la contraseña introducida → 200.
- Revisar las demás rutas de lectura de gpre (
/occurrences/...) por si también olvidan la contraseña y exponen datos sensibles.
Qué aprender de este caso
- Cuando un plugin envuelve o amplía la API de otro (aquí, eventos recurrentes sobre GatherPress), compara cada ruta nueva con la original: si la original tiene una
permission_callbacky la copia usa__return_true, ya tienes candidato. - En WordPress, la contraseña de entrada solo protege donde alguien llama a
post_password_required(). Las rutas REST propias no la heredan: busca endpoints que lean datos de una entrada comprobando solo que esté publicada. - Para probarlo, lanza la misma petición anónima contra la ruta oficial y contra la alternativa: un 401 frente a un 200 con datos es la prueba más clara de que una de las dos se salta el control.
- Al programar un envoltorio, reutiliza la función de permisos del plugin base (como
Event::can_read_rsvps()) en vez de reescribir comprobaciones parciales, y cúbrelo con un test del caso protegido.