
# Analyse technique de CVE-2026-26717, une attaque temporelle HMAC dans l'authentification webhook d'OpenFUN Richie LMS, incluant le code vulnérable, l'impact et le correctif utilisant hmac.compare_digest.
Auteur : Elvin Latifli
Identifiant CVE : CVE-2026-26717
Gravité : Moyenne (CVSS 3.1 : 4.8 — AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N)
Produit concerné : OpenFUN Richie (toutes les versions antérieures au commit a1b5bbd)
Composant concerné : src/richie/apps/courses/api.py — sync_course_runs_from_request
Commit de correction : a1b5bbda
Richie est un CMS open-source développé par France Université Numérique (OpenFUN), construit sur DjangoCMS. Il est conçu pour aider les organisations à créer des portails d'éducation en ligne complets avec des catalogues de cours synchronisés depuis des systèmes de gestion de l'apprentissage (LMS) tels qu'OpenEdX. Richie gère la synchronisation des données de cours via des webhooks authentifiés — l'un d'eux contenait la vulnérabilité décrite dans ce rapport.
Le point de terminaison du webhook sync_course_runs_from_request utilisait l'opérateur standard == pour comparer l'en-tête Authorization entrant avec la signature HMAC attendue :
# VULNÉRABLE
signature_is_valid = any(
authorization_header == get_signature(message, secret)
for secret in getattr(settings, "RICHIE_COURSE_RUN_SYNC_SECRETS", [])
)
L'opérateur == effectue une comparaison non constante en temps — il s'arrête dès que le premier caractère non correspondant est trouvé. En envoyant de nombreuses requêtes conçues avec soin et en mesurant les différences de temps de réponse, un attaquant distant peut déduire la signature correcte caractère par caractère, forgeant ainsi un jeton d'authentification valide et contournant entièrement l'authentification du webhook.
Une attaque réussie permet à un attaquant distant non authentifié de :
La correction remplace == par hmac.compare_digest() de Python, conçu pour une comparaison de chaînes en temps constant et éliminant le canal auxiliaire temporel :
# CORRIGÉ
signature_is_valid = any(
hmac.compare_digest(authorization_header, get_signature(message, secret))
for secret in getattr(settings, "RICHIE_COURSE_RUN_SYNC_SECRETS", [])
)