?devis=4821 devenu ?devis=4822 par simple curiosité dans la barre d’adresse : un seul chiffre changé dans l’URL du simulateur en ligne d’un cabinet d’assurance suffisait à afficher intégralement le devis d’un autre client, coordonnées et montants compris, sans la moindre demande d’authentification.
Le simulateur, développé comme extension WordPress sur mesure, calculait une estimation de prime à partir d’un formulaire en plusieurs étapes, puis enregistrait chaque résultat dans une table personnalisée avec un identifiant auto-incrémenté classique, exposé tel quel dans l’URL de la page de résultat.
Un identifiant conçu pour la base, exposé au public
La clé primaire auto-incrémentée d’une table SQL répond à un besoin d’unicité et de performance interne. Elle n’a jamais été pensée comme un secret ni comme un droit d’accès : sa prévisibilité même est une propriété recherchée pour l’indexation. Le simulateur reprenait pourtant cette même valeur, sans transformation, comme unique paramètre de la page de résultat.
function afficher_resultat_devis() {
$id = isset( $_GET['devis'] ) ? absint( $_GET['devis'] ) : 0;
global $wpdb;
$devis = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}devis_assurance WHERE id = %d", $id )
);
// Aucune vérification que $devis appartient au visiteur courant.
return generer_html_devis( $devis );
}
La requête elle-même est correctement protégée contre l’injection SQL grâce à $wpdb->prepare(). Le problème ne se situe pas là : il se situe dans l’absence totale de contrôle sur le droit du visiteur à consulter précisément cette ligne, quelle qu’elle soit.
Comment l’énumération a été mise en évidence
Lors de l’audit, une simple boucle de test, exécutée sur un environnement de recette autorisé, a permis de récupérer plusieurs centaines de devis en quelques minutes en faisant varier l’identifiant de façon séquentielle. Aucun mécanisme de limitation de débit ni de détection d’énumération n’existait sur cette route, qui n’était d’ailleurs même pas déclarée comme une route de l’API REST : il s’agissait d’un simple gabarit de page WordPress recevant un paramètre GET.
- L’identifiant exposé correspondait exactement à la clé primaire de la table SQL.
- Aucune vérification d’appartenance ne comparait le devis demandé à une session ou un compte.
- Aucune limitation de fréquence ne ralentissait une énumération automatisée.

Deux niveaux de correction, pas un seul
La tentation la plus fréquente consiste à remplacer l’identifiant séquentiel par un identifiant opaque, généré aléatoirement. C’est une amélioration réelle, mais partielle : elle rend l’énumération beaucoup plus difficile, elle ne rétablit pas un contrôle d’accès qui n’existait pas. Un identifiant opaque intercepté ou partagé par erreur resterait tout aussi consultable par n’importe qui.
function afficher_resultat_devis() {
$jeton = isset( $_GET['devis'] ) ? sanitize_text_field( $_GET['devis'] ) : '';
global $wpdb;
$devis = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}devis_assurance WHERE jeton_acces = %s AND session_id = %s",
$jeton,
wp_get_session_token()
)
);
return $devis ? generer_html_devis( $devis ) : afficher_erreur_acces();
}
Le correctif retenu combine les deux niveaux : un jeton d’accès opaque, généré via wp_generate_password(), remplace l’identifiant séquentiel dans l’URL, et une vérification d’appartenance liée à la session du visiteur conditionne systématiquement l’affichage.
Un défaut fréquent au-delà de ce seul simulateur
Cette classe de vulnérabilité, connue sous le nom de référence d’objet directe non sécurisée, touche fréquemment les fonctionnalités développées rapidement pour un besoin métier ponctuel : simulateur, générateur de PDF, page de confirmation de commande. Le développement se concentre sur le calcul métier, la question de l’accès arrive après, parfois jamais.
| Approche | Empêche l’énumération | Empêche l’accès non autorisé |
|---|---|---|
| Identifiant séquentiel exposé | Non | Non |
| Identifiant opaque seul | Oui, en pratique | Non |
| Identifiant opaque + contrôle d’appartenance | Oui | Oui |
Conseil retenu de cet audit : un identifiant difficile à deviner reste un identifiant, pas un contrôle d’accès. Les deux se complètent, ils ne se remplacent jamais l’un l’autre.
Notre verdict
Le cabinet a choisi de conserver la logique de calcul du simulateur intacte et de ne toucher qu’à la couche d’accès aux résultats, preuve qu’une telle correction ne demande pas de remettre en cause l’architecture existante. Elle demande simplement de poser la question qui manquait à l’origine : cette page appartient-elle réellement à la personne qui la consulte ?