Tables de base de données personnalisées la sécurité d'abord

Ceci est la deuxième partie d'une série sur les tables de base de données personnalisées dans WordPress. Dans la première partie, nous avons présenté les raisons pour et contre l'utilisation de tables personnalisées. Nous avons examiné certains des détails à prendre en compte - la dénomination des colonnes, les types de colonnes - ainsi que la création d'un tableau. Avant d'aller plus loin, nous devons expliquer comment interagir avec cette nouvelle table. sans encombre. Dans un article précédent, j'ai abordé l'assainissement et la validation en général. Dans ce tutoriel, nous examinerons cela plus en détail dans le contexte des bases de données..

La sécurité lors de l’interaction avec une table de base de données est primordiale - c’est pourquoi nous l’avons abordée tôt dans la série. Si ce n'est pas fait correctement, vous pouvez laisser votre table ouverte à la manipulation via l'injection SQL. Cela pourrait permettre à un pirate d'extraire des informations, de remplacer du contenu ou même de modifier le comportement de votre site - et les dommages qu'ils pourraient causer ne se limitent pas à votre tableau personnalisé..

Supposons que nous souhaitions autoriser les administrateurs à supprimer des enregistrements de notre journal d'activité. Une erreur commune que j'ai vue est la suivante:

 if (! empty ($ _GET ['action']) && 'delete-activity-log' == $ _GET ['action'] && isset ($ _GET ['log_id'])) global $ wpdb; unsafe_delete_log ($ _GET ['log_id']);  function unsafe_delete_log ($ log_id) global $ wpdb; $ sql = "SUPPRIMER DE $ wpdb-> wptuts_activity_log WHERE log_id = $ log_id"; $ supprimé = $ wpdb-> requête ($ sql); 

Alors qu'est-ce qui ne va pas ici? Beaucoup: Ils n'ont pas vérifié les autorisations, donc tout le monde peut supprimer un journal d'activité. Ils n'ont pas non plus vérifié les nonces, de sorte que même avec les vérifications d'autorisation, un utilisateur administrateur pourrait être amené à supprimer un journal. Tout cela a été couvert dans ce tutoriel. Mais leur troisième erreur compose les deux premiers: le unsafe_delete_log () function utilise la valeur transmise dans une commande SQL sans l'échapper au préalable. Cela laisse le champ libre à la manipulation.

Supposons que son utilisation est

 www.unsafe-site.com?action=delete-activity-log&log_id=7

Et si un attaquant avait visité (ou trompé un administrateur): www.unsafe-site.com?action=delete-activity-log&log_id=1;%20DROP%20TABLE%20wp_posts. le log_id contient une commande SQL, qui est ensuite injectée dans $ sql et serait exécuté comme:

 DELETE de wp_wptuts_activity_log WHERE log_id = 1; DROP TABLE wp_posts

Le résultat: l'ensemble wp_posts la table est supprimée. J'ai vu du code comme celui-ci sur les forums - et le résultat est que toute personne visitant leur site peut mettre à jour ou supprimer tout table dans leur base de données.

Si les deux premières erreurs sont corrigées, il est alors plus difficile pour ce type d'attaque de fonctionner - ce n'est pas impossible, et cela ne protégera pas contre un "attaquant" autorisé à supprimer les journaux d'activité. Il est extrêmement important de protéger votre site contre les injections SQL. C’est aussi incroyablement simple: WordPress fournit la préparer méthode. Dans cet exemple particulier:

 function safe_delete_log ($ log_id) global $ wpdb; $ sql = $ wpdb-> prepare ("DELETE de $ wpdb-> wptuts_activity_log WHERE log_id =% d", $ log_id); $ supprimé = $ wpdb-> requête ($ sql)

La commande SQL serait maintenant exécutée comme

 DELETE de wp_wptuts_activity_log WHERE log_id = 1;

Désinfection des requêtes de base de données

La plupart des opérations d’assainissement peuvent être effectuées uniquement à l’aide du $ wpdb global - notamment par son préparer méthode. Il fournit également des méthodes pour insérer et mettre à jour des données dans des tableaux en toute sécurité. Celles-ci fonctionnent généralement en remplaçant une entrée inconnue ou en associant une entrée à un espace réservé de format. Ce format indique à WordPress à quelles données il doit s’attendre:

  • % s dénote une chaîne
  • %ré dénote un entier
  • %F dénote un float

Nous commençons par examiner trois méthodes qui non seulement épurent les requêtes, mais les construisent également pour vous..

Insérer des données

WordPress fournit la méthode $ wpdb-> insert (). C'est un wrapper pour insérer des données dans la base de données et gère la désinfection. Il faut trois paramètres:

  • Nom de la table - le nom de la table
  • Les données - tableau de données à insérer sous forme de paires colonne-> valeur
  • Les formats - tableau de formats pour la valeur correspondante dans le tableau de données (par exemple. % s, %ré,%F)

Notez que les clés des données doivent être des colonnes: s'il y a une clé qui ne correspond pas à une colonne, une erreur peut être levée.

Dans les exemples qui suivent, nous avons explicitement défini les données - mais bien sûr, en général, ces données auraient été fournies par l’utilisateur - ce qui pourrait donc être n'importe quoi. Comme discuté dans cet article, les données devrait ont été validés en premier, de manière à pouvoir renvoyer les erreurs éventuelles à l'utilisateur - mais nous devons encore nettoyer les données avant de les ajouter à notre table. Nous examinerons la validation dans le prochain article de cette série..

 $ wpdb global; // $ user_id = 1; $ activité = 1; $ object_id = 1479; $ activity_date = date_i18n ('Y-m-d H: i: s', faux, vrai); $ insert = $ wpdb-> insert ($ wpdb-> wptuts_activity_log, array ('user_id' => $ user_id, 'activity' => $ activity, 'object_id' => $ object_id, 'activity_date' => $ activity_date,) , tableau ('% d', '% s', '% d', '% s',)); if ($ inséré) $ insert_id = $ wpdb-> insert_id;  else // L'insertion a échoué

Mise à jour des données

Pour mettre à jour les données de la base de données, nous avons $ wpdb-> update (). Cette méthode accepte cinq arguments:

  • Nom de la table - le nom de la table
  • Les données - tableau de données à mettre à jour sous forme de paires colonne-> valeur
  • - tableau de données à faire correspondre sous forme de paires colonne-> valeur
  • Format de données - tableau de formats pour les valeurs de données correspondantes
  • Où Format - tableau de formats pour les valeurs 'where' correspondantes

Ceci met à jour toutes les lignes correspondant au tableau where avec les valeurs du tableau de données. Encore une fois, comme avec $ wpdb-> insert () les clés du tableau de données doivent correspondre à une colonne. Il retourne faux en cas d'erreur, ou le nombre de lignes mises à jour.

Dans l'exemple suivant, nous mettons à jour tous les enregistrements avec l'ID de journal '14' (qui doit être au plus un enregistrement, car il s'agit de notre clé primaire). Il met à jour l'ID utilisateur sur 2 et l'activité sur 'modifiée'.

 $ wpdb global; $ user_id = 2; $ activity = 'edité'; $ log_id = 14; $ updated = $ wpdb-> update ($ wpdb-> wptuts_activity_log, array ('user_id' => $ user_id, 'activity' => $ activity,), array ('log_id' => $ log_id,), array (' % d ','% s ​​'), tableau ('% d '),); if ($ updated) // Nombre de lignes mises à jour = $ updated

Suppression

Depuis la version 3.4, WordPress fournit également la $ wpdb-> delete () méthode permettant de supprimer facilement (et en toute sécurité) une ou plusieurs lignes. Cette méthode prend trois paramètres:

  • Nom de la table - le nom de la table
  • - tableau de données à faire correspondre sous forme de paires colonne-> valeur
  • Les formats - tableau de formats pour le type de valeur correspondant (par exemple. % s, %ré,%F)

Si vous voulez que votre code soit compatible avec WordPress pré-3.4, vous devrez utiliser le $ wpdb-> prepare méthode pour nettoyer l'instruction SQL appropriée. Un exemple de ceci a été donné ci-dessus. le $ wpdb-> delete méthode retourne le nombre de lignes delete ou false sinon - vous pouvez ainsi déterminer si la suppression a réussi.

 $ wpdb global; $ supprimé = $ wpdb-> supprimer ($ wpdb-> wptuts_activity_log, array ('log_id' => 14,), array ('% d'),); if ($ supprimé) // nombre de lignes supprimées = $ supprimé

esc_sql

À la lumière des méthodes ci-dessus et des considérations plus générales $ wpdb-> prepare () méthode discutée ensuite, cette fonction est un peu redondante. Son fourni comme un wrapper utile pour la $ wpdb-> escape () méthode, elle-même glorifiée ajoute des slash. Comme il est généralement plus approprié et souhaitable d’utiliser les trois méthodes ci-dessus, ou $ wpdb-> prepare (), vous constaterez probablement que vous avez rarement besoin d'utiliser esc_sql ().

À titre d'exemple simple:

 $ activity = 'commented'; $ sql = "SUPPRIMER DE $ wpdb-> wptuts_activity_log WHERE.esc_sql ($ activity)." ";";

Requêtes générales

Pour les commandes SQL générales où (c’est-à-dire celles qui n’insèrent, ne suppriment ou ne mettent pas à jour les lignes), nous devons utiliser la méthode $ wpdb-> prepare (). Il accepte un nombre variable d'arguments. La première est la requête SQL que nous souhaitons exécuter avec toutes les données «inconnues» remplacées par leur marque de formatage appropriée. Ces valeurs sont transmises en tant qu'arguments supplémentaires, dans l'ordre dans lequel elles apparaissent.

Par exemple au lieu de:

 $ sql = "SELECT * FROM $ wpdb-> wptuts_activity_log WHERE user_id = $ user_id AND object_id = $ object_id AND activity = $ activity ORDER BY date_activité $ order"; $ logs = $ wpdb-> get_results ($ sql);

on a

 $ sql = $ wpdb-> prepare ("SELECT * FROM $ wpdb-> wptuts_activity_log WHERE id_utilisateur =% d AND object_id =% d AND activité =% s ORDER BY date_activité_s%", $ user_id, $ object_id, $ activity , $ order); $ logs = $ wpdb-> get_results ($ sql);

le préparer méthode fait deux choses.

  1. Ça s'applique mysql_real_escape_string () (ou addedlashes ()) aux valeurs insérées. Cela empêchera notamment les valeurs contenant des guillemets de sortir de la requête..
  2. Ça s'applique vsprintf () lors de l'ajout des valeurs à la requête pour s'assurer qu'elles sont correctement formatées (les entiers sont des entiers, les flottants sont des flottants, etc.). C'est pourquoi notre exemple au tout début de l'article a tout dépouillé sauf le "1"..

Requêtes plus compliquées

Vous devriez trouver ça $ wpdb-> prepare, avec les méthodes d'insertion, de mise à jour et de suppression sont tout ce dont vous avez vraiment besoin. Parfois, bien que dans certaines circonstances, une approche plus «manuelle» soit souhaitée - parfois simplement du point de vue de la lisibilité. Par exemple, supposons que nous ayons un ensemble inconnu d’activités pour lesquelles nous voulons tous les journaux. Nous * pourrions * ajouter dynamiquement le % s espaces réservés à la requête SQL, mais une approche plus directe semble plus facile:

 // Un tableau inconnu qui devrait contenir les chaînes interrogées pour $ activities = array (…); // Désinfecte le contenu du tableau $ activities = array_map ('esc_sql', $ activities); $ activités = array_map ('sanitize_title_for_query', $ activities); // Crée une chaîne à partir du tableau nettoyé formant la partie interne de l'instruction IN (…) $ in_sql = "'". imploser ("','", $ activités). "'"; // Ajoute ceci à la requête $ sql = "SELECT * FROM $ wpdb-> wptuts_activity_log WHERE activity IN ($ in_sql);" // Effectue la requête $ logs = $ wpdb-> get_results ($ sql);

L'idée est d'appliquer esc_sql et sanitize_title_for_query à chaque élément du tableau. Le premier ajoute des barres obliques pour échapper aux termes - semblable à ce que $ wpdb-> prepare () Est-ce que. La seconde s'applique simplement sanitize_title_with_dashes () - bien que le comportement puisse être complètement modifié à travers des filtres. L’instruction SQL proprement dite est formée en implosant le tableau, désormais assaini, dans une chaîne séparée par des virgules, qui est ajoutée à la liste. DANS(… ) partie de la requête.

Si le tableau doit contenir des entiers, il suffit d’utiliser intval () ou absint () pour désinfecter chaque élément du tableau.

Liste blanche

Dans d'autres cas, une liste blanche peut être appropriée. Par exemple, l'entrée inconnue peut être un tableau de colonnes à renvoyer dans la requête. Puisque nous connaissons les colonnes de la base de données, nous pouvons simplement les ajouter à la liste blanche - en supprimant les champs que nous ne reconnaissons pas. Cependant, pour rendre notre code convivial, nous devons être insensibles à la casse. Pour ce faire, nous convertirons tout ce que nous recevons en minuscules - car dans la première partie, nous utilisions spécifiquement des noms de colonnes en minuscules..

 // Un tableau inconnu qui devrait contenir des colonnes à inclure dans la requête $ fields = array (…); // Une liste blanche de champs autorisés $ allowed_fields = array (…); // Convertit les champs en minuscules (car nos noms de colonnes sont tous en minuscules - voir la première partie) $ fields = array_map ('strtolower', $ fields); // Désinfecte par liste blanche $ fields = array_intersect ($ fields, $ allowed_fields); // Renvoie uniquement les champs sélectionnés. Les champs vides $ sont interprétés comme tous si (vide (champs $)) $ sql = "SELECT * FROM $ wpdb-> wptuts_activity_log";  else $ sql = "SELECT" .implode (',', champs $). "FROM $ wpdb-> wptuts_activity_log";  // Effectuer la requête $ logs = $ wpdb-> get_results ($ sql);

La liste blanche est également pratique pour régler COMMANDÉ PAR partie de la requête (si celle-ci est définie par la saisie utilisateur): les données peuvent être commandées en tant que DESC ou ASC seulement.

 // Entrée utilisateur inconnue (supposée être asc ou desc) $ order = $ _GET ['order']; // Autorise les entrées à être quelconques ou mixtes, cas $ order = strtoupper ($ order); // Valeur de la commande désinfectée $ order = ('ASC' == $ order? 'ASC': 'DEC');

LIKE Requêtes

Les instructions SQL LIKE prennent en charge l'utilisation de caractères génériques tels que % (zéro ou plusieurs caractères) et _ (exactement un caractère) lors de la mise en correspondance des valeurs avec la requête. Par exemple la valeur foobar correspondrait à n'importe laquelle des requêtes:

 SELECT * FROM $ wpdb-> wptuts_activity_log WHERE activité LIKE 'foo%' SELECT * FROM $ wpdb-> wptuts_activity_log WHERE activité LIKE '% bar' SELECT * FROM $ wpdb-> wptd_activity_log WHERE activité LIKE '% oba%' SELECT * FROM $ wpdb-> wptuts_activity_log en tant qu'activité similaire à 'fo_bar%'

Cependant, ces caractères spéciaux peuvent effectivement être présents dans le terme recherché - et afin d'éviter qu'ils ne soient interprétés comme des caractères génériques - nous devons les échapper. Pour cela, WordPress fournit le like_escape () une fonction. Notez que cela n’empêche pas l’injection SQL - mais échappe seulement à la % et _ personnages: vous devez toujours utiliser esc_sql () ou $ wpdb-> prepare ().

 // Récupère le terme $ term = $ _GET ['activity']; // échappe aux caractères génériques $ term = like_escape ($ term); $ sql = $ wpdb-> prepare ("SELECT * FROM $ wpdb-> wptuts_activity_log WHERE activité LIKE% s", '%'. $ terme. '%'); $ logs = $ wpdb-> get_results ($ sql);

Fonctions d'encapsulation des requêtes

Dans les exemples que nous avons examinés, nous avons utilisé deux autres méthodes de $ wpdb:

  • $ wpdb-> query ($ sql) - Ceci effectue toute requête qui lui est donnée et renvoie le nombre de lignes affectées..
  • $ wpdb-> get_results ($ sql, $ ouput) - Ceci effectue la requête qui lui est donnée et retourne le jeu de résultats correspondant (c'est-à-dire les lignes correspondantes). $ sortie définit le format des résultats renvoyés:
    • ARRAY_A - tableau numérique de lignes, où chaque ligne est un tableau associatif, indexé par les colonnes.
    • ARRAY_N - tableau numérique de lignes, où chaque ligne est un tableau numérique.
    • OBJET - tableau numérique de lignes, où chaque ligne est un objet de ligne. Défaut.
    • OBJECT_K - tableau associatif de lignes (indexé par la valeur de la première colonne), chaque ligne étant un tableau associatif.

Il y en a d'autres que nous n'avons pas mentionnés aussi:

  • $ wpdb-> get_row ($ sql, $ sortie, $ rangée) - Ceci effectue la requête et retourne une ligne. $ row définit quelle ligne doit être renvoyée; par défaut, la valeur est 0, la première ligne correspondante. $ sortie définit le format de la ligne:
    • ARRAY_A - Row est un colonne => valeur paire.
    • ARRAY_N - La ligne est un tableau numérique de valeurs.
    • OBJET - La ligne est retournée sous forme d'objet. Défaut.
  • $ wpdb-> get_col ($ sql, $ colonne) - Ceci effectue la requête et retourne un tableau numérique de valeurs de la colonne spécifiée.. $ colonne spécifie la colonne à renvoyer sous forme d'entier. Par défaut c'est 0, la première colonne.
  • $ wpdb-> get_var ($ sql, $ colonne, $ rangée) - Cela effectue la requête et renvoie une valeur particulière. $ row et $ colonne sont comme ci-dessus et spécifient la valeur à renvoyer. Par exemple,
     $ activities_by_user_1 = $ wpdb-> get_var ("SELECT COUNT (*) FROM $ wpdb-> wptuts_activity_log WHERE id_utilisateur = 1");

Il est important de noter que ces méthodes ne sont que des wrappers pour effectuer une requête SQL et formater le résultat.. Ils ne désinfectent pas la requête - vous ne devriez donc pas les utiliser seuls lorsque la requête contient des données "inconnues".


Résumé

Nous avons abordé pas mal de choses dans ce didacticiel - et la désinfection des données est un sujet important à comprendre. Dans le prochain article, nous l'appliquerons à notre plug-in. Nous étudierons le développement d’un ensemble de fonctions d’emballage (similaires aux fonctions telles que wp_insert_post (), wp_delete_post () etc.) qui ajoutera une couche d'abstraction entre notre plug-in et la base de données.