Reports
MEDIA[APIS]#4029867

WordPress: la API gpre/v1 de events.wordpress.org mostraba los asistentes de eventos protegidos con contraseña

Un envoltorio REST propio (gpre/v1) de la plataforma de eventos de WordPress devolvía, sin sesión ni contraseña, la lista de asistentes de eventos recurrentes protegidos que la API oficial de GatherPress sí bloqueaba.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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.

  1. Crear un gatherpress_event, publicarlo y ponerle contraseña de entrada (por ejemplo secret-pass).
  2. Configurarlo como serie recurrente con los metadatos de gpre (semanal u otra frecuencia), para que se generen ocurrencias y exista un recurrence_id (por ejemplo 20260921T100000).
  3. Con un usuario normal con sesión iniciada, confirmar asistencia a esa ocurrencia, de modo que gpre asocie el comentario RSVP a ella.
  4. Cerrar sesión (o usar un navegador limpio, sin cookies) y no introducir la contraseña del evento.
  5. 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>
  1. 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
        }
      ]
    }
  }
}
  1. Comprobar que en la petición no iba ninguna cookie wp-postpass_* ni cabecera Authorization.

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_callback de gpre/v1/.../rsvp-responses la misma comprobación que GatherPress, por ejemplo Event::can_read_rsvps( (int) $request['post_id'] ), como hace la ruta oficial con can_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_callback y 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.