« Réponse invalide, réessayez. » Ce message s’affichait dans les journaux d’un module de quiz e-learning pour un compte apprenant qui, d’après les traces de navigation classiques, n’avait jamais ouvert la page du quiz correspondant. Aucun clic, aucun affichage de question, et pourtant une tentative de correction en bonne et due forme arrivait sur le serveur. L’anomalie méritait d’être creusée avant d’être classée comme un simple bug d’affichage.
L’inspection du trafic réseau côté navigateur, puis côté serveur, a fini par révéler la source : certains apprenants appelaient directement l’endpoint REST de correction automatique, sans jamais charger l’interface du quiz, pour tester des combinaisons de réponses jusqu’à obtenir le score attendu. Le correcteur automatique, pensé comme un simple aller-retour entre le front-end et l’API, ne vérifiait jamais si la tentative correspondait à une session de quiz réellement engagée.
Un endpoint de correction sans mémoire de session
Le module reposait sur une route REST enregistrée ainsi :
register_rest_route( 'quiz/v1', '/correct', array(
'methods' => 'POST',
'callback' => 'quiz_corriger_tentative',
'permission_callback' => function() {
return is_user_logged_in();
},
) );
Le permission_callback vérifiait bien qu’un utilisateur était connecté, ce qui semblait suffisant à première vue. Mais rien n’empêchait cet utilisateur légitime d’appeler la route autant de fois qu’il le souhaitait, avec n’importe quelle combinaison de réponses, sans jamais avoir affiché les questions. La fonction de correction elle-même comparait simplement le tableau de réponses reçu à un tableau de bonnes réponses stocké côté serveur, sans lien avec une tentative réellement démarrée.
Reconstituer une véritable machine à états côté serveur

La correction a consisté à introduire un jeton de tentative généré au moment où l’apprenant démarre effectivement le quiz, et à ne plus accepter de correction sans ce jeton valide :
function quiz_demarrer_tentative( $quiz_id, $user_id ) {
$jeton = wp_generate_password( 32, false );
set_transient( 'quiz_tentative_' . $jeton, array(
'quiz_id' => $quiz_id,
'user_id' => $user_id,
'demarre_le' => time(),
), HOUR_IN_SECONDS );
return $jeton;
}
function quiz_corriger_tentative( WP_REST_Request $request ) {
$jeton = sanitize_text_field( $request->get_param( 'jeton' ) );
$tentative = get_transient( 'quiz_tentative_' . $jeton );
if ( ! $tentative || $tentative['user_id'] !== get_current_user_id() ) {
return new WP_Error( 'tentative_invalide', 'Aucune tentative en cours.', array( 'status' => 403 ) );
}
delete_transient( 'quiz_tentative_' . $jeton );
// ... comparaison des réponses avec le quiz_id validé côté serveur
}
Le jeton n’est valable qu’une fois, expire au bout d’une heure, et lie la correction à un utilisateur et un quiz précis stockés côté serveur, jamais transmis tels quels par le client. Un appel direct à l’endpoint sans être passé par la page de démarrage du quiz ne renvoie plus qu’une erreur 403.
Pourquoi vérifier la réponse finale ne suffit pas
- Un correcteur qui compare uniquement les réponses soumises aux bonnes réponses peut être bombardé de combinaisons jusqu’à trouver la bonne, sans limite de tentative.
- Sans jonction avec une session réellement démarrée, rien ne distingue un appel légitime d’un script qui teste des permutations.
- La temporalité compte aussi : un quiz censé durer dix minutes ne devrait pas accepter une correction soumise trois secondes après son démarrage.
Ajouter une limite de tentatives et un délai minimal
Au-delà du jeton de session, deux garde-fous supplémentaires ont été ajoutés pour couper court à toute automatisation résiduelle :
- Un compteur de tentatives par quiz et par utilisateur, stocké en meta utilisateur, qui bloque au-delà d’un nombre raisonnable défini par le formateur.
- Un contrôle de délai minimal entre le démarrage et la correction, rejetant toute soumission trop rapide pour avoir été lue par un humain.
Ces deux vérifications se font entièrement côté serveur, dans le même callback de correction, sans dépendre d’un minuteur JavaScript qu’un script pourrait ignorer en appelant directement l’API.
Un correcteur automatique protège une note, pas seulement un affichage : il mérite la même rigueur de validation qu’un formulaire de paiement.
Ce qu’on retient de cet incident
Le réflexe naturel est de protéger l’interface visible d’un quiz — masquer les bonnes réponses dans le HTML, chiffrer le JavaScript, obscurcir le code. Cet incident rappelle qu’aucune de ces protections ne compte si l’API sous-jacente reste, elle, accessible sans contrainte. Toute logique de notation, de validation ou de progression exposée via une route REST doit être conçue en partant du principe qu’un client peut l’appeler directement, sans jamais passer par l’interface prévue.
Pour aller plus loin
La documentation officielle de l’API REST de WordPress détaille la construction des permission_callback et leur articulation avec les capacités utilisateur, un point de départ solide pour concevoir tout endpoint qui touche à une logique métier sensible, bien au-delà du seul cas des quiz e-learning.