Toma de control de cuenta en el plugin de reservas LatePoint (CVE-2026-118066)
Una inyección SQL no autenticada en LatePoint permite tomar el control de una cuenta de cliente. Corregido en 5.7.7; 5.7.8 completa el endurecimiento.
Resumen
- Un atacante no autenticado puede restablecer la contraseña de un cliente de LatePoint con
IS NOT NULLcomo token. Wordfence informó del fallo, Jakub Herman figura en los créditos, y el CVE se publicó el 7 de noviembre de 2026. - LatePoint 5.7.6 y anteriores están afectados. El plugin tiene 100,000 instalaciones activas. El fallo está en el generador de consultas que convierte el centinela en SQL, alcanzado por dos rutas AJAX públicas.
- El registro CVE lo califica CVSS v3.1 9.1 Crítico. No se informa de explotación en entornos reales, y el CVE no está en el catálogo KEV de la CISA al 7 de noviembre de 2026.
- Actualice a 5.7.8. Si no puede, bloquee los dos patrones de solicitud documentados y audite los restablecimientos de contraseña de los clientes; consulte «¿Cómo lo corrijo?».
¿Estoy afectado?
Está expuesto solo cuando se cumplen todas estas condiciones:
- El sitio ejecuta el plugin LatePoint en la versión 5.7.6 o anterior.
- El plugin está activo, por lo que sus rutas AJAX públicas responden a las solicitudes.
- Existe al menos un registro de cliente con un
account_nonseno nulo. LatePoint crea este valor para los clientes, así que un sitio con una sola reserva es suficiente. - El atacante puede obtener la dirección de correo de un cliente. La cadena del aviso lee una de una intención de pedido; una confirmación de reserva pública o un directorio del personal también puede proporcionar una.
- Para el impacto en la cuenta de WordPress vinculada, la opción
wp_users_as_customersestá activada.
El código vulnerable está en las clases de cliente y de consulta del plugin. No se necesita ningún módulo opcional. Las dos rutas son públicas, por lo que el atacante no necesita una cuenta en el sitio.
| Rama | Versiones afectadas | Versión corregida |
|---|---|---|
| LatePoint | 5.7.6 y anteriores | 5.7.7 (2 de noviembre de 2026), luego 5.7.8 (3 de noviembre de 2026) para la corrección completa del modelo |
El registro CVE enumera 5.7.6 y anteriores. La 5.7.7 bloquea las dos rutas documentadas. La 5.7.8 elimina la rama literal del generador, así que use 5.7.8.
Compruebe la versión instalada con WP-CLI:
wp plugin get latepoint --field=version
Sin WP-CLI, lea el encabezado del plugin:
grep -m1 "Version:" wp-content/plugins/latepoint/latepoint.php
Cualquier versión hasta 5.7.6 está afectada.
¿Cómo lo corrijo?
- Actualice LatePoint a 5.7.8 desde el administrador de WordPress, o ejecute
wp plugin update latepoint. La página del plugin indica 5.7.8 como versión actual, publicada el 3 de noviembre de 2026. - Confirme la nueva versión con
wp plugin get latepoint --field=version. - Si el sitio ejecuta una copia en caché de los archivos del plugin, vacíe la caché y confirme que el encabezado del plugin muestra 5.7.8.
- Revise los restablecimientos de contraseña y los inicios de sesión de los clientes del periodo anterior a la actualización. Los pasos de comprobación están en «¿Cómo compruebo si estuve comprometido?».
Si no puede actualizar, desactive LatePoint hasta que pueda. Desactivar el plugin elimina las páginas de reserva y del área de cliente, así que planifique una ventana de mantenimiento.
Una regla de firewall de aplicaciones web puede reducir la exposición, pero no corrige el código. Los dos patrones de solicitud que debe bloquear son:
route_name=customer_cabinet__change_passwordcon un valorpassword_reset_tokenque contengaIS NOT NULL.route_name=steps__start_from_order_intentcon un valororder_intent_keyque contengaIS NOT NULL.
Haga coincidir el valor del parámetro después de decodificar la URL, sin distinguir mayúsculas y minúsculas. Los atacantes pueden variar la codificación, así que una regla que solo coincida con la cadena literal IS+NOT+NULL no detectará otras formas. La regla solo bloquea un restablecimiento legítimo si un token real contiene un espacio, algo que el token hexadecimal del plugin nunca hace.
El proveedor no indica ningún workaround. Restringir las cuentas que pueden acceder al área de cliente no ayuda, porque las rutas son públicas por diseño.
¿Cuál es la causa raíz?
El generador de consultas trata un valor WHERE igual a la cadena literal IS NOT NULL como una palabra clave SQL en lugar de un valor que se debe enlazar. El método OsModel::build_conditions_query() de LatePoint compara cada valor con la cadena y añade la palabra clave al texto SQL. No añade ningún marcador de posición ni valor enlazado para esa rama.
La rama vulnerable en lib/models/model.php en 5.7.6:
} elseif ( $value === 'IS NULL' ) {
// IS NULL
$temp_query = $this->with_table_name( $column ) . ' IS NULL ';
} elseif ( $value === 'IS NOT NULL' ) {
// IS NOT NULL
$temp_query = $this->with_table_name( $column ) . ' IS NOT NULL ';
} elseif ( is_array( $value ) && ! empty( $value ) ) {
$temp_query = $this->with_table_name( $column ) . ' IN ' . OsModel::where_in_array_to_string( $value );
} else {
// Add to list of query values
$where_values[] = $value;
}
El asistente de cliente pasa un valor de solicitud directamente a ese generador:
public static function get_by_account_nonse( $account_nonse ) {
if ( empty( $account_nonse ) ) {
return false;
}
$account_nonse = sanitize_text_field( $account_nonse );
$customer = new OsCustomerModel();
return $customer->where( [ 'account_nonse' => $account_nonse ] )->set_limit( 1 )->get_results_as_models();
}
sanitize_text_field() elimina etiquetas y espacios sobrantes. Conserva los espacios y las palabras clave SQL, así que IS NOT NULL pasa sin cambios. El asistente construye entonces una consulta cuya condición es account_nonse IS NOT NULL. Todos los clientes con un account_nonse no nulo coinciden, y set_limit(1) devuelve una fila.
El punto de entrada de restablecimiento de contraseña usa ese asistente como su comprobación de autenticación:
} elseif ( ! empty( $params['password_reset_token'] ) ) {
$params['password_reset_token'] = sanitize_text_field( $params['password_reset_token'] );
$customer = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );
// Validate token expiration (15 minutes)
if ( $customer ) {
$created_at = OsMetaHelper::get_customer_meta_by_key( 'password_reset_token_created_at', $customer->id );
El punto de entrada trata el cliente devuelto como el propietario del token de restablecimiento. No compara el token con un valor almacenado. La única comprobación restante es la marca de tiempo de frescura de 15 minutos, que el atacante puede iniciar solicitando un restablecimiento para la dirección objetivo.
El mismo patrón aparece en la ruta de intención de pedido. OsOrderIntentHelper::get_order_intent_by_intent_key() pasa intent_key a where(), así que params[order_intent_key]=IS NOT NULL se convierte en intent_key IS NOT NULL y devuelve la primera intención de pedido. El formulario de reserva muestra entonces el nombre y el correo del cliente de ese registro.
La versión 5.7.5 añadió una comprobación is_scalar() a otra búsqueda, la del uuid del cliente en OsStepsHelper::set_customer_object(). Esa comprobación rechaza un array anidado, pero acepta la cadena escalar IS NOT NULL, así que no cierra esta ruta.
¿Cómo se explota?
- El atacante obtiene la dirección de correo de un cliente. La cadena del aviso envía
params[order_intent_key]=IS NOT NULLa la ruta públicasteps__start_from_order_intent. La consulta devuelve la primera intención de pedido, y el formulario de reserva muestra el nombre y el correo del cliente de ese registro. - El atacante solicita un restablecimiento para esa dirección mediante la ruta pública
customer_cabinet__request_password_reset_token, conparams[password_reset_email]=<email>. LatePoint envía un token al cliente y guardapassword_reset_token_created_atpara el cliente. La ventana de 15 minutos comienza. - El atacante envía una solicitud POST a la ruta pública
customer_cabinet__change_passwordconparams[password_reset_token]=IS NOT NULLy una contraseña nueva. El punto de entrada busca al cliente mediante el centinela y devuelve la primera fila coincidente, que el aviso describe como el cliente con el identificador más bajo. - El punto de entrada lee la marca de tiempo de restablecimiento de ese cliente. La marca es reciente por la solicitud de restablecimiento, así que la comprobación de 15 minutos pasa. El punto de entrada llama entonces a
update_password()con la contraseña del atacante. - El atacante inicia sesión como el cliente. La cuenta expone ahora las reservas, los pedidos y los datos personales del cliente. Si
wp_users_as_customersestá activada, la cuenta de WordPress vinculada es accesible mediante el mismo registro de cliente.
El aviso documenta los parámetros de solicitud. Adaptada del aviso, la solicitud de cambio de contraseña tiene esta forma:
POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded
action=latepoint_route_call&route_name=customer_cabinet__change_password¶ms[password_reset_token]=IS+NOT+NULL¶ms[password]=<new-password>¶ms[password_confirmation]=<new-password>
La solicitud de intención de pedido tiene la misma forma con route_name=steps__start_from_order_intent y params[order_intent_key]=IS+NOT+NULL.
El atacante no necesita el token enviado por correo. El centinela sustituye la búsqueda del token, y la solicitud de restablecimiento solo inicia el temporizador de frescura.
¿Cómo se desarrolla el ataque?
Un atacante no autenticado convierte una cadena de solicitud en una condición SQL. Toma el control del primer cliente coincidente e inicia el temporizador de restablecimiento que comprueba el punto de entrada.
¿Qué puede hacer un atacante?
Un atacante no autenticado puede tomar el control de una cuenta de cliente de LatePoint por solicitud. Puede leer o modificar las reservas, los pedidos y los datos personales de ese cliente. La cuenta sobrescrita es la primera fila que devuelve la consulta del centinela, que el aviso describe como el cliente con el identificador más bajo.
El impacto depende de estas condiciones:
- El sitio ejecuta LatePoint 5.7.6 o anterior y el plugin está activo.
- El atacante puede alcanzar las rutas AJAX públicas.
- Un registro de cliente tiene un
account_nonseno nulo. - El atacante puede obtener la dirección de correo de un cliente y solicitar un restablecimiento para ella.
- La marca de tiempo de restablecimiento del cliente está dentro de la ventana de 15 minutos cuando llega la solicitud de cambio de contraseña.
- Para la cuenta de WordPress vinculada,
wp_users_as_customersestá activada.
El registro CVE puntúa la confidencialidad y la integridad como altas y la disponibilidad como nula. El atacante puede leer los datos del cliente y cambiar la contraseña, pero el fallo no ejecuta código y no cambia el ámbito de seguridad.
El plugin vincula cuentas de WordPress solo a roles seguros para clientes. La cuenta vinculada es un suscriptor o un cliente, no un administrador, salvo que el sitio haya cambiado esa configuración.
El aviso no demuestra una ruta hacia la cuenta de administrador de WordPress. Describe la toma de control de una cuenta de cliente, con la cuenta de suscriptor o cliente de WordPress vinculada cuando la opción está activada.
¿Por qué funciona la corrección?
La corrección impide que la cadena de solicitud llegue a la rama de la palabra clave SQL. LatePoint 5.7.7 añade una comprobación estricta de caracteres en los dos puntos de entrada documentados. LatePoint 5.7.8 elimina la rama literal IS NOT NULL del generador de consultas, así que solo un objeto marcador interno puede solicitar la palabra clave.
La protección 5.7.7 en lib/controllers/customer_cabinet_controller.php:
} elseif ( ! empty( $params['password_reset_token'] ) ) {
$params['password_reset_token'] = sanitize_text_field( $params['password_reset_token'] );
- $customer = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );
+ // Reset token is a hex key - a value with spaces/operators (e.g. "IS NOT NULL") is never valid.
+ if ( is_string( $params['password_reset_token'] ) && ctype_alnum( $params['password_reset_token'] ) ) {
+ $customer = OsCustomerHelper::get_by_account_nonse( $params['password_reset_token'] );
+ }
La protección 5.7.7 en lib/controllers/steps_controller.php:
public function start_from_order_intent() {
- $order_intent = OsOrderIntentHelper::get_order_intent_by_intent_key( $this->params['order_intent_key'] );
+ // Intent key is a hex token - a value with spaces/operators (e.g. "IS NOT NULL") is never valid.
+ $order_intent_key = sanitize_text_field( $this->params['order_intent_key'] ?? '' );
+ $order_intent = ( is_string( $order_intent_key ) && ctype_alnum( $order_intent_key ) ) ? OsOrderIntentHelper::get_order_intent_by_intent_key( $order_intent_key ) : new OsOrderIntentModel();
ctype_alnum() rechaza el espacio en IS NOT NULL, así que la búsqueda se omite y la solicitud falla de forma segura. La misma versión añade los objetos marcadores null_condition() y not_null_condition(). También añade los asistentes where_null() y where_not_null(), para que el código interno pueda seguir expresando una comparación NULL sin una cadena de solicitud.
El cambio 5.7.8 en lib/models/model.php elimina la rama literal:
- } elseif ( $value === 'IS NULL' || $value === self::null_condition() ) {
+ } elseif ( $value === self::null_condition() || ( $value === 'IS NULL' && self::is_legacy_null_condition() ) ) {
// IS NULL
$temp_query = $this->with_table_name( $column ) . ' IS NULL ';
- } elseif ( $value === 'IS NOT NULL' || $value === self::not_null_condition() ) {
+ } elseif ( $value === self::not_null_condition() ) {
// IS NOT NULL
$temp_query = $this->with_table_name( $column ) . ' IS NOT NULL ';
} elseif ( is_array( $value ) && ! empty( $value ) ) {
Después de 5.7.8, la cadena IS NOT NULL cae en la rama de valor enlazado. El literal IS NULL permanece solo para un complemento LatePoint Pro antiguo, y solo cuando la versión del complemento es anterior a 1.7.7. El registro CVE trata 5.7.7 como la versión corregida. El cambio 5.7.8 cierra la ruta a nivel del modelo para cualquier otro llamador que pase datos de solicitud a where().
¿Cómo compruebo si estuve comprometido?
Empiece por los registros que capturan el cuerpo de la solicitud, como un registro de auditoría de firewall de aplicaciones web o un registro de solicitudes de proxy inverso. LatePoint lee la ruta y los parámetros del cuerpo POST, así que un registro de acceso estándar solo registra la solicitud admin-ajax.php. Si el atacante envía los parámetros en la cadena de consulta, el registro de acceso los contiene.
grep -Ei 'route_name=(customer_cabinet__change_password|steps__start_from_order_intent)' /var/log/apache2/*access*.log /var/log/nginx/*access*.log /var/log/modsec_audit.log 2>/dev/null
Busque el centinela en las líneas coincidentes, en forma decodificada de URL y sin distinguir mayúsculas y minúsculas. El valor puede aparecer como IS+NOT+NULL, IS%20NOT%20NULL o una codificación mixta. Una coincidencia es una pista, no una prueba, porque un escáner puede enviar la misma cadena.
Compruebe las marcas de tiempo de restablecimiento de clientes que no esperaba:
wp db query "SELECT object_id, meta_value FROM wp_latepoint_customer_meta WHERE meta_key='password_reset_token_created_at' ORDER BY meta_value DESC LIMIT 50;"
Reemplace wp_ por el prefijo de sus tablas. Se escribe una marca de tiempo de restablecimiento cuando LatePoint envía un correo de restablecimiento. Compare las filas con sus registros de soporte. Un restablecimiento que el cliente no solicitó, seguido de un inicio de sesión desde una dirección desconocida, es una señal más fuerte.
LatePoint no mantiene un registro de auditoría de los cambios de contraseña de los clientes. Compare los avisos de cambio de contraseña de los clientes con las filas de restablecimiento. Revise también el registro web para los inicios de sesión en el área de cliente en el mismo periodo.
El proveedor no publica indicadores de compromiso. Un intento fallido no deja ningún rastro específico del proveedor, y un intento exitoso puede dejar solo la contraseña cambiada y la marca de tiempo de restablecimiento.
Cronología
- 27 de octubre de 2026: se reserva CVE-2026-118066.
- 2 de noviembre de 2026: se publica LatePoint 5.7.7 con las dos protecciones en los puntos de entrada y los asistentes de marcador.
- 3 de noviembre de 2026: se publica LatePoint 5.7.8 y elimina la rama literal
IS NOT NULLdel generador de consultas. - 7 de noviembre de 2026: se publica el registro CVE a las 11:26 UTC, y el aviso GHSA-rmmm-8g4j-3qgc se publica a las 12:31 UTC.
Preguntas frecuentes
¿El atacante necesita una cuenta en el sitio?
No. Las dos rutas están registradas para visitantes no conectados, y el punto de entrada de cambio de contraseña omite la comprobación del nonce cuando recibe un token de restablecimiento. El atacante solo necesita acceso de red al sitio.
¿Basta con 5.7.7?
Bloquea las dos rutas de solicitud documentadas. El generador de consultas de 5.7.7 todavía trata la cadena literal IS NOT NULL como SQL, así que otro llamador que pase datos de solicitud a where() todavía puede alcanzar la rama. La versión 5.7.8 elimina esa rama. Actualice a 5.7.8.
¿Puede el atacante tomar el control de la cuenta de administrador de WordPress?
No por defecto. El fallo toma el control de un registro de cliente de LatePoint. El plugin vincula un cliente a una cuenta de WordPress solo cuando wp_users_as_customers está activada, y limita ese vínculo a roles seguros para clientes. Un sitio que haya cambiado la asignación de roles debe revisar su configuración.
¿Se explotó en entornos reales?
No se informa de explotación en entornos reales al 7 de noviembre de 2026, y el CVE no está en el catálogo KEV de la CISA. El aviso incluye los parámetros de solicitud, así que la técnica es pública.
¿Qué hago si no puedo actualizar de inmediato?
Desactive LatePoint hasta que pueda actualizar. Si el flujo de reserva debe seguir en línea, bloquee los dos patrones de solicitud en el firewall de aplicaciones web y audite las marcas de restablecimiento a diario. La regla de firewall reduce la exposición pero no corrige el generador de consultas.
¿Una solicitud fallida demuestra que el sitio es seguro?
No. El centinela devuelve el primer cliente coincidente, así que una solicitud puede tener éxito aunque el atacante haya adivinado mal el correo para el temporizador de restablecimiento. Un cambio de contraseña fallido puede haber coincidido con un cliente y haber fallado solo en la comprobación de frescura. Revise los registros y las filas de restablecimiento juntos.
Referencias
- Aviso de GitHub GHSA-rmmm-8g4j-3qgc, publicado el 7 de noviembre de 2026, con el rango afectado, las referencias al código vulnerable y los parámetros de solicitud: https://github.com/advisories/GHSA-rmmm-8g4j-3qgc (comprobado el 7 de noviembre de 2026)
- Registro CVE-2026-118066, publicado el 7 de noviembre de 2026, CVSS v3.1 9.1 y CWE-287: https://www.cve.org/CVERecord?id=CVE-2026-118066 (comprobado el 7 de noviembre de 2026)
- Aviso de inteligencia de amenazas de Wordfence para CVE-2026-118066, el registro de la autoridad de numeración: https://www.wordfence.com/threat-intel/vulnerabilities/id/2f7f5dc8-8cf7-493d-b012-89ff36a4dc02 (comprobado el 7 de noviembre de 2026)
- Registro de versiones de LatePoint, 5.7.7 el 2 de noviembre de 2026 y 5.7.8 el 3 de noviembre de 2026: https://wordpress.org/plugins/latepoint/#developers (comprobado el 7 de noviembre de 2026)
- LatePoint
lib/models/model.phpen 5.7.6, la ramaIS NOT NULL: https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/models/model.php#L411 (comprobado el 7 de noviembre de 2026) - LatePoint
lib/helpers/customer_helper.phpen 5.7.6,get_by_account_nonse(): https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/helpers/customer_helper.php#L228 (comprobado el 7 de noviembre de 2026) - LatePoint
lib/controllers/customer_cabinet_controller.phpen 5.7.6,change_password(): https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/customer_cabinet_controller.php#L468 (comprobado el 7 de noviembre de 2026) - LatePoint
lib/controllers/steps_controller.phpen 5.7.6,start_from_order_intent(): https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/steps_controller.php#L187 (comprobado el 7 de noviembre de 2026) - Changeset de LatePoint 3779410, el cambio de seguridad referenciado por el aviso: https://plugins.trac.wordpress.org/changeset?reponame=&old=3779410%40latepoint&new=3779410%40latepoint (comprobado el 7 de noviembre de 2026)
- LatePoint
lib/models/model.phpen 5.7.8, la rama del objeto marcador: https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.8/lib/models/model.php#L455 (comprobado el 7 de noviembre de 2026) - API de plugins de WordPress.org,
latepoint, 100,000 instalaciones activas y 1,841,969 descargas: https://wordpress.org/plugins/latepoint/ (comprobado el 7 de noviembre de 2026) - Feed JSON del catálogo de vulnerabilidades explotadas de la CISA, comprobado para CVE-2026-118066 el 7 de noviembre de 2026 (no listado): https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (comprobado el 7 de noviembre de 2026)