Neutron, notre moteur d’IA, a obtenu un score de 97,1 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Analyse approfondie

Analyse approfondie

Prise de contrôle de compte dans le plugin de réservation LatePoint (CVE-2026-118066)

Une injection SQL non authentifiée dans LatePoint permet de prendre le contrôle d'un compte client. Corrigé en 5.7.7 ; 5.7.8 complète le durcissement.

En bref

  • Un attaquant non authentifié peut réinitialiser le mot de passe d'un compte client LatePoint avec IS NOT NULL comme jeton. Wordfence a signalé la faille, Jakub Herman est crédité, et le dossier CVE a été publié le 7 novembre 2026.
  • LatePoint 5.7.6 et les versions antérieures sont concernés. Le plugin compte 100 000 installations actives. La faille se situe dans le constructeur de requêtes qui transforme le sentinelle en SQL, atteint par deux routes AJAX publiques.
  • Le dossier CVE la note CVSS v3.1 9,1 Critique. Aucune exploitation dans la nature n'est signalée, et la CVE n'est pas dans le catalogue KEV de la CISA au 7 novembre 2026.
  • Mettez à jour vers 5.7.8. Si vous ne pouvez pas, bloquez les deux motifs de requête documentés et auditez les réinitialisations de mot de passe des clients ; voir « Comment corriger ? ».

Suis-je concerné ?

Vous êtes exposé uniquement lorsque toutes ces conditions sont réunies :

  • Le site exécute le plugin LatePoint en version 5.7.6 ou antérieure.
  • Le plugin est actif, donc ses routes AJAX publiques répondent aux requêtes.
  • Au moins un enregistrement client existe avec un account_nonse non nul. LatePoint crée cette valeur pour les clients, donc un site avec une seule réservation suffit.
  • L'attaquant peut obtenir l'adresse e-mail d'un client. La chaîne de l'avis en lit une depuis une intention de commande ; une confirmation de réservation publique ou un annuaire du personnel peut aussi en fournir une.
  • Pour l'impact sur le compte WordPress lié, l'option wp_users_as_customers est activée.

Le code vulnérable se trouve dans les classes client et de requête du plugin. Aucun module optionnel n'est nécessaire. Les deux routes sont publiques, donc l'attaquant n'a pas besoin de compte sur le site.

Branche Versions concernées Version corrigée
LatePoint 5.7.6 et antérieures 5.7.7 (2 novembre 2026), puis 5.7.8 (3 novembre 2026) pour la correction complète du modèle

Le dossier CVE liste 5.7.6 et antérieures comme concernées. La 5.7.7 bloque les deux chemins documentés. La 5.7.8 supprime la branche littérale du constructeur, donc utilisez 5.7.8.

Vérifiez la version installée avec WP-CLI :

wp plugin get latepoint --field=version

Sans WP-CLI, lisez l'en-tête du plugin :

grep -m1 "Version:" wp-content/plugins/latepoint/latepoint.php

Toute version jusqu'à 5.7.6 est concernée.

Comment corriger ?

  1. Mettez à jour LatePoint vers 5.7.8 via l'administration WordPress, ou exécutez wp plugin update latepoint. La page du plugin liste 5.7.8 comme version actuelle, publiée le 3 novembre 2026.
  2. Confirmez la nouvelle version avec wp plugin get latepoint --field=version.
  3. Si le site exécute une copie mise en cache des fichiers du plugin, videz le cache et confirmez que l'en-tête du plugin affiche 5.7.8.
  4. Examinez les réinitialisations de mot de passe et les connexions des clients pour la période précédant la mise à jour. Les étapes de vérification figurent dans « Comment vérifier si j'ai été compromis ? ».

Si vous ne pouvez pas mettre à jour, désactivez LatePoint jusqu'à ce que ce soit possible. La désactivation du plugin supprime les pages de réservation et d'espace client, donc planifiez une fenêtre de maintenance.

Une règle de pare-feu applicatif web peut réduire l'exposition, mais elle ne corrige pas le code. Les deux motifs de requête à bloquer sont :

  • route_name=customer_cabinet__change_password avec une valeur password_reset_token qui contient IS NOT NULL.
  • route_name=steps__start_from_order_intent avec une valeur order_intent_key qui contient IS NOT NULL.

Faites correspondre la valeur du paramètre après décodage URL, sans tenir compte de la casse. Les attaquants peuvent varier l'encodage, donc une règle qui ne correspond qu'à la chaîne littérale IS+NOT+NULL manquera d'autres formes. La règle ne bloque une réinitialisation légitime que si un vrai jeton contient un espace, ce que le jeton hexadécimal du plugin ne fait jamais.

Le fournisseur ne liste aucun contournement. Restreindre les comptes qui peuvent atteindre l'espace client n'aide pas, car les routes sont publiques par conception.

Quelle est la cause racine ?

Le constructeur de requêtes traite une valeur WHERE égale à la chaîne littérale IS NOT NULL comme un mot-clé SQL au lieu d'une valeur à lier. La méthode OsModel::build_conditions_query() de LatePoint compare chaque valeur avec la chaîne et ajoute le mot-clé au texte SQL. Elle n'ajoute ni espace réservé ni valeur liée pour cette branche.

La branche vulnérable dans 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;
}

L'assistant client transmet directement une valeur de requête à ce constructeur :

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() supprime les balises et les espaces superflus. Elle conserve les espaces et les mots-clés SQL, donc IS NOT NULL passe inchangé. L'assistant construit alors une requête dont la condition est account_nonse IS NOT NULL. Tous les clients dont le champ account_nonse est non nul correspondent, et set_limit(1) renvoie une ligne.

Le point de terminaison de réinitialisation du mot de passe utilise cet assistant comme contrôle d'authentification :

} 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 );

Le point de terminaison traite le client renvoyé comme le propriétaire du jeton de réinitialisation. Il ne compare pas le jeton à une valeur stockée. Le seul contrôle restant est l'horodatage de fraîcheur de 15 minutes, que l'attaquant peut démarrer en demandant une réinitialisation pour l'adresse cible.

Le même schéma apparaît dans la route d'intention de commande. OsOrderIntentHelper::get_order_intent_by_intent_key() transmet intent_key à where(), donc params[order_intent_key]=IS NOT NULL devient intent_key IS NOT NULL et renvoie la première intention de commande. Le formulaire de réservation affiche alors le nom et l'e-mail du client de cet enregistrement.

La version 5.7.5 a ajouté une vérification is_scalar() à une autre recherche, celle de l'uuid client dans OsStepsHelper::set_customer_object(). Cette vérification rejette un tableau imbriqué, mais elle accepte la chaîne scalaire IS NOT NULL, donc elle ne ferme pas ce chemin.

Comment la faille est-elle exploitée ?

  1. L'attaquant obtient l'adresse e-mail d'un client. La chaîne de l'avis envoie params[order_intent_key]=IS NOT NULL à la route publique steps__start_from_order_intent. La requête renvoie la première intention de commande, et le formulaire de réservation affiche le nom et l'e-mail du client de cet enregistrement.
  2. L'attaquant demande une réinitialisation pour cette adresse via la route publique customer_cabinet__request_password_reset_token, avec params[password_reset_email]=<email>. LatePoint envoie un jeton au client et enregistre password_reset_token_created_at pour ce client. La fenêtre de 15 minutes commence.
  3. L'attaquant envoie une requête POST à la route publique customer_cabinet__change_password avec params[password_reset_token]=IS NOT NULL et un nouveau mot de passe. Le point de terminaison recherche le client via le sentinelle et renvoie la première ligne correspondante, que l'avis décrit comme le client ayant le plus petit identifiant.
  4. Le point de terminaison lit l'horodatage de réinitialisation de ce client. L'horodatage est récent à cause de la demande de réinitialisation, donc le contrôle de 15 minutes passe. Le point de terminaison appelle alors update_password() avec le mot de passe de l'attaquant.
  5. L'attaquant se connecte en tant que client. Le compte expose désormais les réservations, les commandes et les données personnelles du client. Si wp_users_as_customers est activée, le compte WordPress lié est accessible via le même enregistrement client.

L'avis documente les paramètres de requête. Adapté de l'avis, la requête de changement de mot de passe a cette forme :

POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded

action=latepoint_route_call&route_name=customer_cabinet__change_password&params[password_reset_token]=IS+NOT+NULL&params[password]=<new-password>&params[password_confirmation]=<new-password>

La requête d'intention de commande a la même forme avec route_name=steps__start_from_order_intent et params[order_intent_key]=IS+NOT+NULL.

L'attaquant n'a pas besoin du jeton envoyé par e-mail. Le sentinelle remplace la recherche du jeton, et la demande de réinitialisation ne fait que démarrer le minuteur de fraîcheur.

Comment se déroule l'attaque ?

Un attaquant non authentifié transforme une chaîne de requête en condition SQL. Il prend le contrôle du premier client correspondant et démarre le minuteur de réinitialisation que le point de terminaison vérifie.

flowchart LR A["Requête non authentifiée"] --> B["password_reset_token défini à IS NOT NULL"] B --> C["sanitize_text_field conserve la chaîne"] C --> D["build_conditions_query émet account_nonse IS NOT NULL"] D --> E["set_limit(1) renvoie le premier client"] E --> F["Un horodatage de réinitialisation récent passe le contrôle de 15 minutes"] F --> G["Le mot de passe de l'attaquant est écrit dans le compte client"]

Que peut faire un attaquant ?

Un attaquant non authentifié peut prendre le contrôle d'un compte client LatePoint par requête et lire ou modifier les réservations, les commandes et les données personnelles de ce client. Le compte écrasé est la première ligne renvoyée par la requête sentinelle, que l'avis décrit comme le client ayant le plus petit identifiant.

L'impact dépend de ces conditions :

  • Le site exécute LatePoint 5.7.6 ou antérieur et le plugin est actif.
  • L'attaquant peut atteindre les routes AJAX publiques.
  • Un enregistrement client a un account_nonse non nul.
  • L'attaquant peut obtenir l'adresse e-mail d'un client et demander une réinitialisation pour elle.
  • L'horodatage de réinitialisation du client se situe dans la fenêtre de 15 minutes lorsque la demande de changement de mot de passe arrive.
  • Pour le compte WordPress lié, wp_users_as_customers est activée.

Le dossier CVE note la confidentialité et l'intégrité comme élevées et la disponibilité comme nulle. L'attaquant peut lire les données du client et changer le mot de passe, mais la faille n'exécute pas de code et ne change pas le périmètre de sécurité.

Le plugin lie les comptes WordPress uniquement à des rôles sûrs pour les clients. Le compte lié est donc un abonné ou un client, pas un administrateur, sauf si le site a modifié cette configuration.

L'avis ne démontre pas de chemin vers le compte administrateur WordPress. Il décrit la prise de contrôle d'un compte client, avec le compte abonné ou client WordPress lié lorsque l'option est activée.

Pourquoi le correctif fonctionne-t-il ?

Le correctif empêche la chaîne de requête d'atteindre la branche du mot-clé SQL. LatePoint 5.7.7 ajoute une vérification stricte des caractères aux deux points d'entrée documentés. LatePoint 5.7.8 supprime la branche littérale IS NOT NULL du constructeur de requêtes, donc seul un objet marqueur interne peut demander le mot-clé.

La garde 5.7.7 dans 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 garde 5.7.7 dans 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() rejette l'espace dans IS NOT NULL, donc la recherche est ignorée et la requête échoue de manière sûre. La même version ajoute les objets marqueurs null_condition() et not_null_condition(). Elle ajoute aussi les assistants where_null() et where_not_null(), pour que le code interne puisse encore exprimer une comparaison NULL sans chaîne de requête.

La modification 5.7.8 dans lib/models/model.php supprime la branche littérale :

-                    } 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 ) ) {

Après 5.7.8, la chaîne IS NOT NULL tombe dans la branche de valeur liée. Le littéral IS NULL reste uniquement pour un ancien module complémentaire LatePoint Pro, et seulement lorsque la version du module est antérieure à 1.7.7. Le dossier CVE traite 5.7.7 comme la version corrigée. La modification 5.7.8 ferme le chemin au niveau du modèle pour tout autre appelant qui transmet des données de requête à where().

Comment vérifier si j'ai été compromis ?

Commencez par les journaux qui capturent le corps de la requête, comme un journal d'audit de pare-feu applicatif web ou un journal de requêtes de proxy inverse. LatePoint lit la route et les paramètres dans le corps POST, donc un journal d'accès standard n'enregistre que la requête admin-ajax.php. Si l'attaquant envoie les paramètres dans la chaîne de requête, le journal d'accès les contient.

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

Cherchez le sentinelle dans les lignes correspondantes, sous forme décodée URL et sans tenir compte de la casse. La valeur peut apparaître comme IS+NOT+NULL, IS%20NOT%20NULL ou un encodage mixte. Une correspondance est une piste, pas une preuve, car un scanner peut envoyer la même chaîne.

Vérifiez les horodatages de réinitialisation pour les clients que vous n'attendiez pas :

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;"

Remplacez wp_ par le préfixe de vos tables. Un horodatage de réinitialisation est écrit lorsque LatePoint envoie un e-mail de réinitialisation. Comparez les lignes avec vos dossiers de support. Une réinitialisation que le client n'a pas demandée, suivie d'une connexion depuis une adresse inconnue, est un signal plus fort.

LatePoint ne tient pas de journal d'audit des changements de mot de passe des clients. Comparez les signalements de changement de mot de passe des clients avec les lignes de réinitialisation. Examinez aussi le journal web pour les connexions à l'espace client autour de la même période.

Le fournisseur ne publie aucun indicateur de compromission. Une tentative échouée ne laisse aucune trace spécifique au fournisseur, et une tentative réussie peut ne laisser que le mot de passe modifié et l'horodatage de réinitialisation.

Chronologie

  • 27 octobre 2026 : la CVE-2026-118066 est réservée.
  • 2 novembre 2026 : LatePoint 5.7.7 est publié avec les deux gardes aux points d'entrée et les assistants de marqueur.
  • 3 novembre 2026 : LatePoint 5.7.8 est publié et supprime la branche littérale IS NOT NULL du constructeur de requêtes.
  • 7 novembre 2026 : le dossier CVE est publié à 11:26 UTC, et l'avis GHSA-rmmm-8g4j-3qgc est publié à 12:31 UTC.

Les deux versions correctives sont sorties en deux jours, et le dossier CVE a suivi quatre jours après 5.7.8.
Figure 1 : De la correction 5.7.7 à 5.7.8 et au dossier CVE. Source : journal des versions de LatePoint et dossier CVE, vérifiés le 7 novembre 2026

FAQ

L'attaquant a-t-il besoin d'un compte sur le site ?

Non. Les deux routes sont enregistrées pour les visiteurs non connectés. Le point de terminaison de changement de mot de passe ignore la vérification du nonce lorsqu'il reçoit un jeton de réinitialisation. L'attaquant a seulement besoin d'un accès réseau au site.

La version 5.7.7 suffit-elle ?

Elle bloque les deux chemins de requête documentés. Le constructeur de requêtes 5.7.7 traite encore la chaîne littérale IS NOT NULL comme du SQL. Un autre appelant qui transmet des données de requête à where() peut donc encore atteindre la branche. La version 5.7.8 supprime cette branche. Mettez à jour vers 5.7.8.

L'attaquant peut-il prendre le contrôle du compte administrateur WordPress ?

Non par défaut. La faille prend le contrôle d'un enregistrement client LatePoint. Le plugin lie un client à un compte WordPress uniquement lorsque wp_users_as_customers est activée, et il limite ce lien à des rôles sûrs pour les clients. Un site qui a modifié l'attribution des rôles doit revoir sa configuration.

La faille a-t-elle été exploitée dans la nature ?

Aucune exploitation dans la nature n'est signalée au 7 novembre 2026, et la CVE n'est pas dans le catalogue KEV de la CISA. L'avis inclut les paramètres de requête, donc la technique est publique.

Que faire si je ne peux pas mettre à jour tout de suite ?

Désactivez LatePoint jusqu'à pouvoir mettre à jour. Si le parcours de réservation doit rester en ligne, bloquez les deux motifs de requête au pare-feu applicatif web et auditez les horodatages de réinitialisation chaque jour. La règle de pare-feu réduit l'exposition mais ne corrige pas le constructeur de requêtes.

Une requête échouée prouve-t-elle que le site est sûr ?

Non. Le sentinelle renvoie le premier client correspondant, donc une requête peut réussir même si l'attaquant a deviné le mauvais e-mail pour le minuteur de réinitialisation. Un changement de mot de passe échoué peut tout de même avoir correspondu à un client et échoué seulement au contrôle de fraîcheur. Examinez les journaux et les lignes de réinitialisation ensemble.

Références

  • Avis GitHub GHSA-rmmm-8g4j-3qgc, publié le 7 novembre 2026, avec la plage concernée, les références au code vulnérable et les paramètres de requête : https://github.com/advisories/GHSA-rmmm-8g4j-3qgc (vérifié le 7 novembre 2026)
  • Dossier CVE-2026-118066, publié le 7 novembre 2026, CVSS v3.1 9.1 et CWE-287 : https://www.cve.org/CVERecord?id=CVE-2026-118066 (vérifié le 7 novembre 2026)
  • Avis de renseignement sur les menaces Wordfence pour CVE-2026-118066, le dossier de l'autorité de numérotation : https://www.wordfence.com/threat-intel/vulnerabilities/id/2f7f5dc8-8cf7-493d-b012-89ff36a4dc02 (vérifié le 7 novembre 2026)
  • Journal des versions de LatePoint, 5.7.7 le 2 novembre 2026 et 5.7.8 le 3 novembre 2026 : https://wordpress.org/plugins/latepoint/#developers (vérifié le 7 novembre 2026)
  • LatePoint lib/models/model.php en 5.7.6, la branche IS NOT NULL : https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/models/model.php#L411 (vérifié le 7 novembre 2026)
  • LatePoint lib/helpers/customer_helper.php en 5.7.6, get_by_account_nonse() : https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/helpers/customer_helper.php#L228 (vérifié le 7 novembre 2026)
  • LatePoint lib/controllers/customer_cabinet_controller.php en 5.7.6, change_password() : https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/customer_cabinet_controller.php#L468 (vérifié le 7 novembre 2026)
  • LatePoint lib/controllers/steps_controller.php en 5.7.6, start_from_order_intent() : https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.6/lib/controllers/steps_controller.php#L187 (vérifié le 7 novembre 2026)
  • Changeset LatePoint 3779410, la modification de sécurité référencée par l'avis : https://plugins.trac.wordpress.org/changeset?reponame=&old=3779410%40latepoint&new=3779410%40latepoint (vérifié le 7 novembre 2026)
  • LatePoint lib/models/model.php en 5.7.8, la branche de l'objet marqueur : https://plugins.trac.wordpress.org/browser/latepoint/tags/5.7.8/lib/models/model.php#L455 (vérifié le 7 novembre 2026)
  • API des plugins WordPress.org, latepoint, 100 000 installations actives et 1 841 969 téléchargements : https://wordpress.org/plugins/latepoint/ (vérifié le 7 novembre 2026)
  • Flux JSON du catalogue des vulnérabilités exploitées de la CISA, vérifié pour CVE-2026-118066 le 7 novembre 2026 (non listée) : https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (vérifié le 7 novembre 2026)