Todos los posts
Encontrando CVEs en WordPress: CVE-2025-3769, IDOR en LatePoint

Encontrando CVEs en WordPress: CVE-2025-3769, IDOR en LatePoint

Esta entrada documentaCVE-2025-3769Ficha completa del CVE →

La mayoría de plugins de WordPress nacen como el proyecto paralelo de alguien. Un desarrollador monta un formulario de reservas para un cliente, ve que funciona y lo sube a wordpress.org. A los seis meses está en diez mil sitios. A los dos años, en cien mil. El código no se ha reescrito en todo ese tiempo. Ha ido acumulando funcionalidades. Analítica. Un panel de admin. Notificaciones al cliente. Una versión premium. Las asunciones de seguridad del primer día no se revisaron nunca.

Ahí vivía CVE-2025-3769. LatePoint es un plugin de calendario y reservas con unas 100.000 instalaciones activas. Un handler AJAX llamado view_booking_summary_in_lightbox, en lib/controllers/customer_cabinet_controller.php, aceptaba un ID de reserva desde la petición y renderizaba un lightbox con el nombre del cliente, su email, la hora de la cita y el servicio. Sin comprobación de propiedad. Le pasas cualquier ID de reserva válido, te devuelve esa reserva.

Eso es un IDOR. CWE-639. El tipo de bug que se enseña en cualquier curso básico de seguridad web. Y que sigue apareciendo en 2025.

Un IDOR es cuando tu aplicación expone una referencia a un objeto interno (un ID de base de datos, una ruta de fichero, un número de cuenta) y confía en que el usuario no la manipule. Si /api/booking?id=42 devuelve la reserva 42 sin comprobar si quien llama tiene permiso para verla, tienes un IDOR. El nombre formal lo resume bien: evasión de autorización mediante clave controlada por el usuario. La decisión de autorización se toma con datos que controla el atacante. Cambia el ID. Cambia el resultado.

Aquí está la función vulnerable, literal de LatePoint 5.1.92, en lib/controllers/customer_cabinet_controller.php línea 67:

public function view_booking_summary_in_lightbox() {
    if ( ! filter_var( $this->params['booking_id'], FILTER_VALIDATE_INT ) ) {
        exit();
    }
    $booking                  = new OsBookingModel( $this->params['booking_id'] );
    $order_item               = new OsOrderItemModel( $booking->order_item_id );
    $order                    = new OsOrderModel( $order_item->order_id );
    $this->vars['booking']    = $booking;
    $this->vars['order_item'] = $order_item;
    $this->vars['order']      = $order;
    $this->format_render( __FUNCTION__ );
}

La validación comprueba una sola cosa: que booking_id sea un entero válido. Si lo es, el handler carga esa reserva por ID, sigue las claves foráneas al order item y al order, y renderiza la plantilla del lightbox con los tres objetos. En ningún momento verifica que la reserva pertenezca a quien llama.

LatePoint no usa el hook estándar wp_ajax_nopriv_ de WordPress. Trae su propio sistema de control de acceso, registrado en el constructor del mismo fichero a partir de la línea 19:

$this->action_access['customer'] = array_merge( $this->action_access['customer'], [
    ...
    'view_booking_summary_in_lightbox',
    ...
] );

Esto dice: la acción requiere un cliente autenticado. Y el controlador base lo respeta. O sea, el bug no es "cualquier visitante puede vaciar la base de datos". El bug es "cualquier cliente puede vaciar la base de datos". En la práctica la diferencia es fina. En un sitio típico con LatePoint, darse de alta como cliente es el propio flujo de reserva. Completas una reserva y tienes cuenta. Sin aprobación de admin. A partir de ahí, el IDOR es un barrido de IDs.

El parche llegó en la 5.1.93. Mismo fichero, misma función, tres líneas añadidas justo después de cargar los modelos:

$booking    = new OsBookingModel( $this->params['booking_id'] );
$order_item = new OsOrderItemModel( $booking->order_item_id );
$order      = new OsOrderModel( $order_item->order_id );

if ( $order->is_new_record() || ( $order->customer_id != OsAuthHelper::get_logged_in_customer_id() ) ) {
    $this->send_json( array( 'status' => LATEPOINT_STATUS_ERROR, 'message' => __('Not Allowed', 'latepoint') ) );
}

El order tiene que existir y su customer_id tiene que coincidir con el del cliente que está en sesión. Si no coincide, no hay datos.

El mismo patrón faltaba en dos funciones hermanas del mismo fichero: view_order_summary_in_lightbox en la línea 57 y scheduling_summary_for_bundle en la línea 46. El changeset de 5.1.93 parcheó las tres. Una pasada de revisión, tres hallazgos. Cuando encuentres un bug con esta forma, mira siempre las funciones vecinas. El desarrollador que escribió una habrá escrito las otras.

Este bug es de abril de 2025. Si lees esto en 2026 y te preguntas si los asistentes LLM cambian el flujo, sí, en los bordes. Pasarle el árbol de código de un plugin a Claude Code o a un buen asistente de programación te da una primera pasada de auditoría en segundos, y te va a sacar exactamente esta clase de comprobaciones de propiedad que faltan. Lo que no desaparece es el juicio. El LLM te va a marcar decenas de cosas, buena parte serán falsos positivos. Saber qué handler _nopriv_ importa, si un ACL propio es efectivo, qué hallazgo merece un reproducer, eso lo sigues haciendo tú leyendo el código. Los asistentes aceleran el grep. No sustituyen el pensamiento.

Y ahora la parte que importa si estás empezando. ¿Cómo encuentras esto tú mismo?

Auditar plugins de WordPress es de los caminos más rápidos para conseguir CVEs publicados si estás entrando en investigación de seguridad. Miles de plugins disponibles. Wordfence asigna CVEs. Patchstack asigna CVEs. El código es público, la instalación es trivial y las mismas cinco o seis clases de vulnerabilidad se repiten una y otra vez.

Ten clara la contrapartida. El dinero es bajo. Tres cifras para severidad media, como mucho cuatro para las críticas. Un bug parecido en un programa privado de HackerOne o Bugcrowd paga entre cinco y diez veces más. Lo que te da el trabajo en plugins de WordPress no es dinero: son CVEs publicados a tu nombre, práctica leyendo código de producción real y un portfolio de hallazgos que puedes enseñar cuando te presentas a un puesto de seguridad o a un cliente de pentest. Si vas a por dinero, vete a programas privados. Si vas a por credenciales y horas de vuelo sobre código realmente vulnerable, empieza aquí.

Este es el flujo que sigo.

Elige un target que no esté ya muy mirado. Los plugins con más de 100.000 instalaciones parecen apetecibles pero los mira todo el mundo con un scanner. Tu report va a duplicar algo que ya está en cola de revisión. Por debajo de mil instalaciones el fabricante puede ni responder a reports de seguridad. El terreno útil está en medio. Entre unos pocos miles y los diez mil pocos es donde el plugin es lo bastante maduro como para que el fabricante te haga caso y lo bastante oscuro como para que los bugs sigan ahí.

Empieza grepando el prefijo de AJAX no autenticado. La mayoría de plugins de WordPress usan el sistema estándar de hooks, no un ACL propio:

grep -rn "wp_ajax_nopriv_" wp-content/plugins/plugin-objetivo/

Cada endpoint AJAX no autenticado que expone un plugin estándar se registra con ese prefijo. Un plugin típico tiene entre cinco y quince. Lee cada uno. Sigue el registro hasta la función handler. Mira qué parámetros coge, qué lee de la base de datos y qué devuelve.

Si el plugin trae su propio ACL (LatePoint, extensiones de WooCommerce, plugins de membresía), grep por el array de control de acceso. Los nombres varían pero la forma es la misma: $action_access, $acl_map, $allowed_actions, una clase de router con buckets public y customer. Sigue esos arrays hasta los handlers que exponen a usuarios no autenticados o de bajo privilegio. La pregunta de auditoría es la misma en los dos estilos: ¿qué hace este endpoint y debería quien llama poder hacerlo?

Busca la ausencia de comprobaciones. No estás buscando bugs. Estás buscando código que falta. Un endpoint bien asegurado tiene alguna combinación de check_ajax_referer() para validar el nonce, current_user_can() para comprobar capacidades, un token o firma atada al objeto al que se accede y una comprobación de propiedad contra el usuario actual. Cero de las cuatro aparecían en el handler de LatePoint. Ese es el patrón.

Traza el flujo de datos. Sigue los parámetros. ¿Dónde va $this->params['booking_id']? A un lookup por modelo. A un render de plantilla. A una consulta SQL. A una lectura de fichero. A un unserialize(). Cada destino es una clase de bug distinta: IDOR, divulgación de ficheros, SQLi, object injection. Los plugins de WordPress suelen tener uno o dos de estos cada mil líneas de código.

No ignores los handlers wp_ajax_ sin la variante _nopriv_. Eso restringe a usuarios autenticados. No restringe a administradores. Un plugin que registra wp_ajax_delete_all_bookings y no comprueba capacidades deja que cualquier usuario de nivel suscriptor lo llame. En sitios que permiten registro abierto, el nivel suscriptor es un alta gratuita. La misma clase de bug con otra variante.

Para reportar, los dos programas centrados en WordPress son Wordfence y Patchstack. Ambos revisan los reports, gestionan la coordinación con el fabricante y emiten el CVE. Los scopes y las tablas de recompensas son diferentes. Hay plugins que están en scope de los dos, otros solo en uno, otros en ninguno. Lee las reglas de cada programa antes de decidir dónde mandar un hallazgo. CVE-2025-3769 fue por Wordfence. El 17 de abril reporté el bug y el 14 de mayo se publicó el CVE con el parche en la 5.1.93. Cuatro semanas de punta a punta, sin contacto directo con el fabricante por mi parte.

La mayor parte de la investigación en seguridad acaba en una de dos categorías. Ataques novedosos y dramáticos que dan para una charla en una conferencia. O el trabajo de limar clases de bugs bien conocidas en código que nadie está revisando. La segunda categoría es donde vive la mayor parte de la exposición real. Código que nadie mira.

Cien mil sitios WordPress con un LatePoint vulnerable significa cien mil sitios donde, durante algunos días, cualquier cliente podía recorrer la base de datos de clientes. Esas son las apuestas reales. Nada exótico. Solo fontanería que nadie estaba revisando.

Si quieres CVEs publicados y horas de vuelo sobre código realmente vulnerable, elige un plugin que no esté ya muy mirado, grep por _nopriv_ y por el array de control de acceso del plugin, lee cada handler. Vas a encontrar bugs.

¿Quieres que encuentre esto en tu aplicación, o aprender a encontrarlo tú?

Solicitar un pentestReservar mentoría
← AnteriorCRTO: una certificación que comprueba si realmente sabes operarSiguiente →Encontrando CVEs en WordPress: CVE-2025-4392, XSS almacenado en Shared Files