Security Blog
Posted By Gregory

Under The Hood 9.8.3


English version: Under The Hood 9.8.3


Cette mise à jour correspond à une phase de maintenance et de renforcement de la sécurité, et non à l'ajout de nouvelles fonctionnalités. L'essentiel du travail a été réalisé dans des domaines rarement consultés par les propriétaires de sites : la couche de données qui analyse les journaux de sécurité, le code qui génère les e-mails de notification et les méthodes d'exportation qui fournissent un fichier CSV de vos enregistrements d'activité. L'apparence du plugin reste inchangée, mais son fonctionnement est considérablement amélioré, notamment en cas de forte affluence sur le site, de journaux volumineux ou d'attaques exploitant des failles que nous n'avions pas encore corrigées.

Vous trouverez ci-dessous le détail des modifications apportées au niveau de l'implémentation, à l'intention des administrateurs et des développeurs qui souhaitent comprendre ce qui a réellement changé.

Fermeture d'un vecteur d'injection de destinataire d'e-mail

WP Cerber envoie deux types d'e-mails transactionnels contenant le nom du destinataire : le message de code PIN pour l'authentification à deux facteurs et la notification d'alerte d'activité. Dans les deux cas, le destinataire est nommé selon la norme RFC 5322 ( Name <email> ) et ce nom est extrait directement des champs du profil WordPress ( user_firstname , user_lastname et display_name ) sans échappement.

Le problème réside dans la manière dont wp_mail() traite les destinataires de type chaîne de caractères. Elle divise la chaîne à la moindre virgule et ne tient pas compte des guillemets. Un utilisateur ayant inséré les caractères adéquats dans son nom de profil pourrait ainsi ajouter une adresse supplémentaire à la liste des destinataires du courriel contenant le code PIN de l'authentification à deux facteurs (2FA) généré par CRB_2FA::send_user_pin() et du courriel d'alerte généré par la branche user_list de cerber_get_email() . Sur un site où chacun peut s'inscrire et définir un nom d'utilisateur, cela représente une réelle possibilité de rediriger discrètement une copie d'un message de sécurité.

Nous avons ajouté un outil de nettoyage dédié, crb_sanitize_mail_display_name() , dans cerber-common.php et l'avons appliqué aux deux sites d'appel. Il supprime précisément les caractères pertinents pour ce type d'injection : la virgule, le guillemet, la barre oblique inverse, les chevrons et les caractères de contrôle. Le nom s'affiche toujours normalement pour les utilisateurs légitimes et la liste des destinataires ne peut plus être manipulée via un champ de profil falsifié.

Suppression d'un vecteur XSS stocké dans le scanner de propriété

Le scanner de logiciels malveillants signale les changements de propriétaire d'un plugin installé sur le dépôt WordPress.org. Pour ce faire, il exploite les métadonnées de propriété fournies par ce dépôt et, jusqu'à présent, intégrait certaines de ces métadonnées dans la notification d'administration sous forme de code HTML brut, y compris les balises d'ancrage créées manuellement pour les liens vers le profil du propriétaire.

Ce dépôt est une source fiable et la probabilité de métadonnées malveillantes est faible, mais les données externes ne doivent jamais parvenir à l'interface d'administration sous forme de balisage non échappé. Le fait de le considérer comme fiable est précisément l'hypothèse sur laquelle nous préférons ne pas nous appuyer.

Pour fermer correctement le vecteur plutôt que de le corriger localement, nous avons introduit un nouvel élément UI Factory, formatted_text , construit via la fonction auxiliaire crb_ui_formatted_text() . Cet élément génère un modèle en texte brut contenant des espaces réservés numérotés %N$s . Les fragments littéraux du modèle et les arguments scalaires sont échappés HTML, tandis que tout argument qui est lui-même un élément d'interface utilisateur est rendu par le moteur de rendu actif. Chaque valeur dynamique est ainsi échappée dans le contexte où elle est réellement émise, ce qui constitue le seul moyen fiable d'éviter les erreurs de contexte de sortie.

Le message de propriété dans crb_check_ownership() utilise désormais cet élément, et les liens vers le profil du propriétaire sont construits avec crb_ui_link() au lieu de chaînes d'ancre concaténées. Ainsi, les URL de profil et les noms d'affichage sont échappés respectivement en tant qu'URL et en tant que texte. La chaîne de traduction reste inchangée, les localisations continuent donc de fonctionner. Le risque XSS lié à l'administrateur stocké est éliminé.

Correction du filtre « Erreur logicielle » dans l’inspecteur de trafic

Dans le journal de l'inspecteur de trafic, le formulaire de recherche avancée permet de combiner plusieurs conditions. L'une d'elles est la case à cocher « Toute erreur logicielle ». Lorsque cette case était cochée avec d'autres filtres, des requêtes ayant enregistré une erreur PHP pouvaient apparaître dans les résultats, même si elles ne correspondaient pas aux autres conditions spécifiées.

La cause première résidait dans la priorité des opérateurs lors de l'assemblage de la clause WHERE. L'ancien code concaténait les fragments de filtre sous forme de chaînes de caractères, et la condition d'erreur groupée n'était pas entre parenthèses. Par conséquent, un OR à l'intérieur pouvait échapper à la logique AND environnant. Lors du transfert de la requête de trafic vers le générateur de requêtes, la condition d'erreur groupée est désormais correctement entre parenthèses, et le filtre combiné se comporte comme prévu. Lorsque vous affinez une recherche, les résultats tiennent désormais compte de toutes les conditions que vous avez définies.

Correspondance des caractères génériques de recherche littéralement

La recherche avancée dans les journaux de trafic autorisait auparavant la saisie de % ou _ dans un terme de recherche à fonctionner comme un métacaractère LIKE , car le terme était inséré dans le modèle sans échappement. Il s'agit au mieux d'une fonctionnalité accidentelle, au pire d'une surcharge inutile.

Chaque terme LIKE est désormais traité par CRB_Database::escape_like() avant l'ajout des caractères génériques environnants. Ainsi, % et _ sont reconnus comme les caractères saisis par l'utilisateur. Les recherches se comportent de manière prévisible et la requête ne peut être forcée à effectuer une analyse plus poussée que prévu.

Correction de la gestion des limites de mémoire pour les exportations

L'exportation d'un journal d'activité ou de trafic volumineux consomme beaucoup de mémoire. C'est pourquoi WP Cerber augmente la mémoire disponible avant l'opération. Dans certains environnements, une valeur numérique de limite de mémoire, telle que 512 était interprétée comme des octets plutôt que des mégaoctets. Dans ce cas, l'extension n'augmentait pas la limite comme prévu et l'exportation pouvait s'interrompre prématurément.

La valeur est désormais interprétée avec l'unité correcte, l'augmentation de mémoire s'applique donc comme prévu et les exportations importantes s'exécutent jusqu'à leur terme dans les environnements qui étaient précédemment affectés.

Reconstruction des exportations de journaux autour du streaming

Les exportations d'activité et de trafic lisaient les lignes correspondantes par blocs, en réexécutant la même requête SELECT avec un OFFSET croissant pour chaque bloc. Sur un journal volumineux, cela se traduit par une analyse en profondeur, où chaque bloc coûte plus cher que le précédent et où le résultat s'accumule en mémoire.

Les deux exportations lisent désormais leurs lignes en une seule passe non tamponnée via CRB_Database::query_stream() , encapsulée dans un générateur qui traite une ligne à la fois. La mémoire reste constante quel que soit le nombre d'enregistrements correspondants, et la base de données effectue le traitement une seule fois au lieu d'une fois par segment. Étant donné qu'un flux non tamponné verrouille la connexion pendant sa consommation, le total de la ligne et la plage de dates sont déterminés au préalable par des requêtes COUNT et MIN / MAX tamponnées distinctes, construites à partir des mêmes filtres, avant l'ouverture du flux.

Deux en-têtes de réponse ont été ajoutés à la fonction d'assistance au téléchargement de fichiers partagés, crb_file_headers() . X-Accel-Buffering: no indique à Nginx, lorsqu'il fait office de proxy pour PHP-FPM, de transmettre chaque segment immédiatement au lieu de mettre en mémoire tampon l'intégralité de l'exportation avant de l'envoyer. Ceci améliore le temps de réponse initial et évite que le proxy ne conserve un fichier CSV volumineux en mémoire. Cet en-tête est spécifique à Nginx et est ignoré sans conséquence par Apache avec mod_php et par d'autres proxys. Cache-Control: no-store empêche le navigateur ou tout proxy intermédiaire de mettre en cache une exportation de journal de sécurité sensible.

Nous avons également rendu le nettoyage du flux déterministe. L'exportation consomme le générateur dans un bloc try / finally et libère le seul handle restant dans le finally . Ainsi, le résultat non mis en mémoire tampon est libéré et la connexion est déverrouillée quelle que soit la sortie : achèvement normal, arrêt prématuré ou exception levée lors de l'écriture d'une ligne. Auparavant, un arrêt prématuré ou une erreur avant la consommation complète pouvait laisser le résultat ouvert et la connexion verrouillée, ce qui entraînait l'échec de la requête suivante.

Rapport de la plage de dates exportées

L'en-tête d'exportation CSV n'affichait auparavant que les filtres actifs. Désormais, les exportations d'activité et de trafic ajoutent deux lignes à l'en-tête, indiquant les horodatages des enregistrements les plus anciens et les plus récents couverts par les données exportées. L'en-tête étant écrit avant la première ligne, la plage de dates provient d'une requête MIN / MAX associée, construite à partir de la même requête filtrée que l'exportation. Cette ligne est omise lorsqu'aucune ligne ne correspond. Lors de l'archivage d'une exportation, le fichier enregistre désormais la période exacte qu'il représente.

Rendre visibles les échecs d'exportation au lieu de les laisser silencieux. Afficher les échecs d'exportation au lieu de les laisser

L'ancienne méthode d'exportation pouvait échouer sans explication. Si la base de données était indisponible ou si le flux de lignes ne pouvait être ouvert, le code précédent générait généralement un fichier CSV vide sans explication, ce qui représente le pire scénario pour une personne tentant d'extraire des enregistrements lors d'un incident.

Les chemins de lecture et d'exportation des deux journaux ont été retravaillés afin de renvoyer un résultat Revalt contenant soit les données utiles, soit une erreur structurée, avec des codes distincts tels que activity_export_query_build_failed , activity_export_db_unavailable et activity_export_stream_failed . Une défaillance de niveau inférieur est intégrée au résultat, préservant ainsi la cause racine initiale. Les erreurs de configuration interrompent désormais l'exportation avec wp_die() avant même l'envoi du premier octet CSV, au lieu de générer un fichier vide.

Lorsqu'une exportation échoue, un administrateur disposant de l'autorisation manage_options voit la cause racine en chaîne, par exemple l'erreur de base de données sous-jacente, ajoutée au message. Les utilisateurs ne disposant pas de cette autorisation ne voient pas ces informations ; ainsi, les détails exploitables parviennent aux personnes compétentes, tandis que les spécificités internes de la base de données restent confidentielles. Le message est échappé via crb_escape_html() .

Consolidation des journaux d'activité et de trafic dans des classes de domaine

Une grande partie de ce cycle était structurelle. Les journaux d'activité et de trafic contenaient des requêtes SQL et une gestion des résultats dispersées dans le code du tableau de bord et des exportations. Nous avons déplacé cette logique dans les classes CRB_Activity et CRB_Traffic_Log ; ainsi, la construction des requêtes et la récupération des lignes s'effectuent désormais via des méthodes claires telles que fetch() et stream_log() au lieu de transiter par le code de présentation sous forme de SQL brut.

Les deux classes construisent désormais leurs requêtes à l'aide du générateur de requêtes DB Warp obtenu via warp_get_db() au lieu de concaténer manuellement les fragments WHERE, JOIN et LIMIT. Ce changement est important, au-delà de la simple amélioration de la clarté du code. Le passage de chaque valeur de filtre fournie par l'utilisateur à travers une seule couche d'échappement élimine le mélange précédent d'échappement manuel, $wpdb->prepare() et de concaténation manuelle entre guillemets, une incohérence qui masquait précisément les failles d'injection. Lorsqu'une couche de base de données dégradée ne peut pas construire une requête, le code génère désormais une erreur sécurisée avec une condition d'absence de correspondance, au lieu d'exécuter une requête non filtrée.

Ces modifications sont internes et ne changent pas ce qui s'affiche à l'écran, mais elles constituent le fondement qui a permis aux correctifs de sécurité et de fiabilité mentionnés ci-dessus d'être petits, locaux et vérifiables.

Deux bogues latents ont été découverts lors de l'extraction du code d'alerte.

Le déplacement de la distribution des alertes d'administration de CRB_Activity::log() vers une classe dédiée CRB_Activity_Alerts a mis en évidence deux bugs préexistants causés par un tableau positionnel dont les indices numériques s'étaient désalignés.

Le premier problème affectait les liens du tableau de bord dans les e-mails d'alerte. L'association clé-valeur (séparée) entraînait un décalage des valeurs ; ainsi, un paramètre de lien comme filter_ip pouvait recevoir le début d'une plage d'adresses IP au lieu de l'adresse attendue. L'association des valeurs à des clés nommées a corrigé ce problème d'alignement, et les liens dans les e-mails d'alerte pointent désormais correctement.

Le deuxième problème concernait la correspondance des utilisateurs dans les chaînes de recherche. Le code appelait wp_get_current_user() , qui renvoie un objet WP_User même pour l'ID utilisateur 0. Par conséquent, la correspondance utilisait une identité incorrecte. Désormais, le système recherche l'utilisateur associé à l'événement avec crb_get_userdata() et se prémunit contre les utilisateurs manquants. Les alertes correspondant à un utilisateur ciblent désormais le bon utilisateur.

Un tableau de bord plus silencieux : priorité à l’opérateur dans le contrôle des modifications

CRB_Activity::is_modified_since() comparait un horodatage avec $stamp < $status['data_modified'] ?? PHP_INT_MAX . La priorité des opérateurs imposait (... < ...) ?? PHP_INT_MAX , ce qui rendait la partie de fusion des valeurs nulles inopérante et provoquait une erreur de clé indéfinie en l'absence data_modified . La fusion étant désormais entre parenthèses, une valeur manquante d'horodatage est traitée comme « modifiée », conformément à la méthode is_modified() , et l'erreur a disparu.

Centralisation des définitions de schémas de base de données

Le dernier élément structurel concerne la maintenance du schéma. Le code d'installation et de mise à niveau utilisait des instructions SQL CREATE TABLE en ligne pour les tables de journalisation, ce qui signifiait qu'une même table pouvait être décrite à plusieurs endroits. Ces déclarations proviennent désormais d'une source unique, CRB_Schema_Definitions , ce qui permet à l'installation et à la mise à niveau de se comporter à partir d'une définition canonique et CRB_Schema_Manager de détecter plus systématiquement les dérives de schéma dans les installations existantes. Les instructions SQL DROP INDEX brutes ont également été remplacées par CRB_Schema_Manager::drop_index_if_exists() .

Nous avons également assoupli le décodage des données stockées dans les champs de requête. Les valeurs héritées pouvant être nulles, les valeurs vides, les JSON invalides et les charges utiles sérialisées non prises en charge sont désormais gérées de la même manière, en les résolvant en un tableau vide au lieu de laisser un enregistrement malformé perturber la lecture.

Pourquoi cette sortie est importante

La version 9.8.3 ne comporte pas de nouveaux boutons. Elle propose en revanche une couche de données sécurisée qui ne laisse plus les erreurs silencieuses, des notifications par e-mail qui ne peuvent plus être manipulées par un nom de profil fictif, une notification d'administration qui protège correctement les données externes, des exportations de journaux exécutées en mémoire vive et signalant les problèmes, ainsi que des filtres de recherche conformes aux instructions du formulaire. Ce sont ces types d'améliorations qui garantissent la fiabilité d'un plugin de sécurité sur le long terme, et nous préférons effectuer ce travail en toute transparence plutôt que de prétendre que le code précédent était déjà parfait.


I'm a team lead in Cerber Tech. I'm a software & database architect, WordPress - PHP - SQL - JavaScript developer. I started coding in 1993 on IBM System/370 (yeah, that was amazing days) and today software engineering at Cerber Tech is how I make my living. I've taught to have high standards for myself as well as using them in developing software solutions.

View Comments
There are currently no comments.