Le WordPress d'aujourd'hui, décodé pour les développeurs

Extensions

L’en-tête Vary sur un point d’entrée REST maison mis en cache en amont

Sans en-tête Vary correct, un cache de reverse-proxy peut servir la réponse d'un utilisateur à un autre. Diagnostic d'un cas réel et correctif d'en-têtes.

Par WordPress Développement • 29 mai 2023 • 5 min de lecture • Aucun commentaire
L'en-tête Vary sur un point d'entrée REST maison mis en cache en amont

« Je vois les informations de compte de quelqu’un d’autre sur cette page. » Ce signalement, remonté par un utilisateur via le support, décrivait un scénario alarmant sur un point d’entrée REST personnalisé qui renvoyait les préférences d’affichage propres à chaque utilisateur connecté. Deux utilisateurs, connectés à quelques minutes d’intervalle depuis la même page, avaient reçu la réponse destinée à l’autre.

Le diagnostic a d’abord semblé pointer vers une faille de sécurité côté PHP, une variable de session mal isolée ou un problème d’authentification. La cause réelle se trouvait ailleurs : un cache de reverse-proxy placé en amont du serveur WordPress, configuré pour mettre en cache les réponses de l’API REST par mesure de performance, sans distinguer les réponses selon l’utilisateur qui les avait générées.

Symptôme : une réponse partagée entre deux utilisateurs

Le point d’entrée en cause, /wp-json/extension-prefs/v1/utilisateur, renvoyait les préférences d’affichage de l’utilisateur authentifié courant, en lisant l’identité via wp_get_current_user() à l’intérieur du callback de la route REST. Fonctionnellement correct côté PHP, ce code produisait bien une réponse différente pour chaque utilisateur.

Le reverse-proxy placé devant l’application, configuré pour accélérer les réponses de l’API sur les mêmes URL fréquemment appelées, ne savait pas que cette URL identique produisait un contenu différent selon l’utilisateur. Sans indication contraire, il traitait la première réponse obtenue comme valable pour toute requête ultérieure sur cette même URL, et la servait telle quelle au visiteur suivant.

Diagnostic : l’absence de Vary

L'essentiel à retenir : Le symptôme apparaît sous forme de données d'un autre utilisateur affichées ; Le cache d'amont ignore tout ce que Vary ne lui signale pas ; Le correctif se limite à quelques lignes d'en-têtes HTTP

L’en-tête HTTP Vary, envoyé dans une réponse, indique à tout cache intermédiaire (reverse-proxy, CDN, navigateur) sur quels autres en-têtes de la requête initiale la réponse dépend, en plus de l’URL elle-même. Sans cet en-tête, un cache HTTP conforme aux standards considère par défaut que la réponse ne dépend que de l’URL, et la réutilise donc pour toute requête identique en apparence.

Une vérification avec curl -I sur le point d’entrée concerné confirmait l’absence totale d’en-tête Vary dans la réponse : rien n’indiquait au reverse-proxy que le cookie d’authentification, transmis dans l’en-tête Cookie de la requête, changeait le contenu de la réponse.

curl -I https://exemple.fr/wp-json/extension-prefs/v1/utilisateur \
  -H "Cookie: wordpress_logged_in_xxx=..."

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: public, max-age=300
# Aucun en-tête Vary présent

La fonction register_rest_route() ne gère pas nativement l’ajout d’en-têtes de cache personnalisés : ceux-ci s’ajoutent via l’objet WP_REST_Response retourné par le callback, en appelant header() ou la méthode set_headers() de la réponse avant son envoi final.

register_rest_route( 'extension-prefs/v1', '/utilisateur', [
    'methods'             => 'GET',
    'callback'            => 'extension_prefs_get_utilisateur',
    'permission_callback' => static function () {
        return is_user_logged_in();
    },
] );

function extension_prefs_get_utilisateur( WP_REST_Request $request ) {
    $user = wp_get_current_user();
    $prefs = get_user_meta( $user->ID, 'extension_prefs', true );

    $response = new WP_REST_Response( $prefs ?: new stdClass() );
    $response->header( 'Vary', 'Cookie' );
    $response->header( 'Cache-Control', 'private, no-store' );

    return $response;
}

Deux décisions ont été prises conjointement : ajouter Vary: Cookie pour tout cache qui respecterait cet en-tête, et surtout retirer complètement la possibilité de mise en cache publique via Cache-Control: private, no-store, car une donnée strictement personnelle ne devrait jamais transiter par un cache partagé, même correctement segmenté par utilisateur.

Vérification et prévention

  • Revérifier avec curl -I depuis deux sessions authentifiées différentes que les réponses ne sont plus jamais partagées.
  • Étendre l’audit à tous les points d’entrée REST personnalisés de l’extension qui renvoient une donnée liée à l’utilisateur courant.
  • Ajouter, dans la checklist de revue de code de l’équipe, une vérification systématique de l’en-tête Cache-Control sur toute route REST personnalisée exposant une donnée utilisateur.

La leçon que je retiens de ce cas : un cache en amont du serveur applicatif ne connaît que ce que les en-têtes HTTP lui disent explicitement. Le silence sur ce point n’est jamais neutre, il se traduit par la pire hypothèse possible pour la confidentialité des données.

En résumé

Une réponse REST qui dépend de l’utilisateur authentifié doit toujours porter des en-têtes de cache explicites, faute de quoi un cache d’amont bien intentionné peut mélanger les réponses de deux utilisateurs différents. Ce diagnostic porte sur un point d’entrée précis mal configuré ; la configuration générale du cache de page d’un site, avec ses propres règles d’exclusion, reste un sujet distinct.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi