Développer des extensions et des thèmes en toute sécurité


Jusqu’à présent, vous vous êtes familiarisé avec les bases du développement d’extensions et de thèmes pour WordPress. Ce serait le moment idéal pour prendre également en compte les implications de sécurité liées à l’écriture de code.

Dans cette leçon, vous allez apprendre à développer en gardant la sécurité à l’esprit.

Vous découvrirez les avantages de garantir la sécurité de votre code, les étapes à suivre pour sécuriser votre code et où trouver davantage d’informations sur le développement en gardant la sécurité à l’esprit.

Avertissement

Cette leçon a été créée comme une introduction au développement d’extensions et de thèmes axé sur la sécurité. Le code utilisé dans cette leçon est très simplifié. Vous ne devez utiliser aucun des codes de ce tutoriel dans vos extensions ou vos thèmes.

Veuillez lire la documentation complète sur la sécurité dans le manuel du développeur WordPress à l’adresse https://developer.wordpress.org/apis/security/ afin de vous assurer que vous suivez les méthodes et procédures appropriées.

Que signifie développer en gardant la sécurité à l’esprit ?

Développer en gardant la sécurité à l’esprit consiste à s’assurer que votre code fonctionne, mais qu’il n’introduit pas non plus de vulnérabilités de sécurité.

Si le code de votre extension ou de votre thème présente des vulnérabilités de sécurité, il pourrait exposer à des attaques potentielles tout site WordPress sur lequel votre produit est installé et entraîner la compromission de ce site.

Lorsque vous écrivez du code, il est important d’adopter une mentalité axée sur la sécurité et de réfléchir à la manière dont votre code pourrait être utilisé à des fins malveillantes.

  • Ne faites confiance à aucune donnée, qu’il s’agisse de données saisies par l’utilisateur, de données provenant d’une API tierce ou même de données stockées dans votre base de données. Vérifiez toujours qu’elles sont valides et sûres à utiliser.
  • WordPress propose un certain nombre d’API qui peuvent vous aider à effectuer des tâches courantes, comme nettoyer les données saisies par l’utilisateur, valider les données et échapper les sorties. Utilisez ces API pour vous aider à valider et à nettoyer vos données plutôt que d’écrire vos propres fonctions.
  • Tenez-vous informé des vulnérabilités courantes et maintenez votre code à jour pour les éviter.

À quel moment la sécurité intervient-elle dans le processus de développement ?

La sécurité doit être prise en compte à chaque étape du processus de développement.

En général, le plus grand nombre de vulnérabilités de sécurité découvertes se situent au niveau PHP, qui est exécuté sur le serveur.

Cette leçon se concentrera donc sur les principales mesures préventives à prendre dans votre code PHP.

Nettoyage des données entrantes

L’une des premières étapes du développement consiste à s’assurer que toutes les données saisies par l’utilisateur sont nettoyées. Cela signifie que toutes les données transmises par l’utilisateur, comme l’envoi d’un formulaire ou un paramètre d’URL, sont vérifiées pour s’assurer qu’elles peuvent être utilisées en toute sécurité.

Dans cet exemple de code, les champs de nom et d’adresse e-mail sont envoyés depuis un formulaire généré par une extension, puis enregistrés dans une table personnalisée de la base de données appelée form_submissions :

$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

Comme vous pouvez le constater, les données sont enregistrées directement dans la base de données, sans aucun nettoyage.

Cela signifie que si un utilisateur envoyait le nom John'; DROP TABLE form_submissions;--, la requête SQL INSERT serait exécutée, suivie de la requête DROP, et la table serait supprimée de la base de données !

WordPress propose une API de nettoyage qui peut être utilisée pour nettoyer les données entrantes. Vous pouvez utiliser les fonctions sanitize_text_field et sanitize_email pour nettoyer les champs de nom et d’adresse e-mail avant leur utilisation dans la requête.

$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );

global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

Remarquez que le code suit un principe essentiel du nettoyage des données : effectuer cette opération le plus tôt possible.

Pour en savoir plus sur les fonctions de nettoyage disponibles pour les développeurs WordPress, consultez la page Nettoyage des données de la documentation du développeur WordPress.

Validation des données (4:06)

Valider des données consiste à les tester par rapport à un modèle prédéfini (ou plusieurs modèles), avec un résultat définitif : valide ou non valide.

Les données non fiables peuvent provenir de nombreuses sources : utilisateurs, données d’API tierces, voire données de votre base de données, qui peuvent également être considérées comme non fiables, surtout si elles ont été modifiées par un autre élément. Même les administrateurs d’un site peuvent faire une erreur et saisir des données incorrectes ou dangereuses. Il est donc important de toujours vérifier vos données.

Dans cet exemple, une fonction de suppression exige qu’un identifiant numérique soit envoyé à un rappel admin-ajax :

add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

Ici, l’identifiant est utilisé directement dans la requête SQL, sans aucune validation. Cela signifie à nouveau que si un utilisateur envoyait l’identifiant 1; DROP TABLE form_submissions;, la même requête SQL DROP serait exécutée après la suppression et la table serait supprimée.

Pour éviter cela, vous pouvez utiliser la fonctionnalité de conversion de type de PHP, afin de vous assurer que la valeur de $id est toujours un entier. Pour ce faire, ajoutez (int) avant le nom de la variable, comme ceci :

$id = (int) $_POST['id'];

Notez que cela ne fonctionnera que si le premier caractère de la chaîne transmise via le tableau $_POST peut être converti en entier ; sinon, la valeur de $id sera 0. Dans ce cas, il est préférable de mettre à jour le code pour gérer cette situation.

Le code suit également un principe essentiel de la validation des données : effectuer cette opération le plus tôt possible.

if ($id === 0){
    // return early with an error
    return wp_send_json( array( 'result' => 'Invalid ID passed' ) );

}

Pour en savoir plus sur les différentes manières de valider les données, consultez la section Validation des données de la documentation du développeur WordPress.

Échapper les sorties

Un autre aspect de la sécurité consiste à s’assurer que toutes les informations que vous envoyez au navigateur sont sûres, notamment le texte, le HTML ou le code JavaScript, ainsi que les données provenant de la base de données.

Même si votre code n’est pas responsable de la source des données affichées, il est responsable de leur affichage sécurisé.

Dans cet exemple, le code récupère les envois du formulaire depuis la base de données, puis parcourt ces envois et affiche leurs données dans un écran d’administration du tableau de bord WordPress :

    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name; ?></td>
                    <td><?php echo $submission->email; ?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php

Ici, trois éléments de données doivent être échappés : les champs $submission->name et $submission->email, ainsi que $submission->id.

<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

Pour les champs de nom et d’adresse e-mail, nous pouvons utiliser la fonction d’échappement intégrée de WordPress esc_html(). L’identifiant peut être échappé en le convertissant en entier, comme vous pourriez le faire pour la validation des données.

  <td><?php echo esc_html( $submission->name ); ?></td>
  <td><?php echo esc_html( $submission->email ); ?></td>
  <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

Remarquez que ce code suit un principe essentiel de l’échappement des données : échapper les données le plus tard possible.

Pour en savoir plus sur les différentes manières d’échapper les sorties, consultez la section Échappement des données de la documentation du développeur WordPress.

Empêcher les requêtes non valides

Lorsqu’une requête est effectuée, il est important de vérifier qu’elle est valide. Cela signifie vérifier qu’elle provient d’une source fiable.

Par exemple, vous pouvez disposer d’un code court qui affiche un formulaire permettant aux utilisateurs d’envoyer leurs informations.

La fonction d’affichage du formulaire pourrait ressembler à ceci.

add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
    ob_start();
    ?>
    <form method="post">
        <input type="hidden" name="wp_learn_form" value="submit">
        <div>
            <label for="email">Name</label>
            <input type="text" id="name" name="name" placeholder="Name">
        </div>
        <div>
            <label for="email">Email address</label>
            <input type="text" id="email" name="email" placeholder="Email address">
        </div>
        <div>
            <input type="submit" id="submit" name="submit" value="Submit">
        </div>
    </form>
    <?php
    $form = ob_get_clean();
    return $form;
}
?>

Lorsque l’utilisateur envoie le formulaire, les données sont transmises au serveur. Elles sont ensuite traitées, nettoyées et stockées dans la base de données.

add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }

    $name = sanitize_text_field( $_POST['name'] );
    $email = sanitize_email( $_POST['email'] );

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

Comme le formulaire peut apparaître sur n’importe quelle page où le code court est utilisé, un utilisateur malveillant pourrait tenter d’envoyer une requête POST au formulaire, soit à la recherche d’une vulnérabilité dans l’extension, soit en envoyant plusieurs requêtes au formulaire.

Pour éviter cela, vous pouvez vérifier que la requête provient d’une source fiable. Pour ce faire, vous pouvez implémenter ce que l’on appelle un nonce, c’est-à-dire un nombre utilisé une seule fois.

Tout d’abord, dans le formulaire lui-même, vous pouvez ajouter un champ nonce en utilisant la fonction wp_nonce_field et en lui transmettant une action nonce ainsi qu’un nom nonce :

wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );

Lorsque le formulaire est affiché sur l’interface publique, un champ masqué est ajouté au formulaire, en utilisant le nom du nonce comme attributs id et name du champ masqué, et le nonce généré comme valeur du champ. Ces données sont envoyées lors de la soumission du formulaire.

Ensuite, dans la fonction qui traite les données du formulaire, vous pouvez vérifier que le nonce est valide en utilisant la fonction wp_verify_nonce et en lui transmettant le champ nonce envoyé via POST ainsi que l’action nonce. Si le résultat de cette vérification est false, vous pouvez quitter la fonction prématurément et empêcher toute exécution ultérieure du code.

    if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

Chaque fois que votre code effectue une requête web, qu’il s’agisse d’une redirection vers une nouvelle URL, de l’envoi de données POST à un formulaire ou d’une requête AJAX, vous devez vérifier que la requête est valide.

Pour en savoir plus sur l’utilisation des nonces dans vos extensions, consultez la section Nonces de la documentation du développeur WordPress.

Empêcher les utilisateurs non authentifiés

Selon les fonctionnalités de votre code, il est recommandé de limiter certaines fonctions aux utilisateurs disposant d’un niveau d’autorisation spécifique. Vous pouvez par exemple disposer d’une fonction qui supprime des données de la base de données.

function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = (int) $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

Même si vous faites de votre mieux pour empêcher les personnes non autorisées d’exécuter cette fonction, il est tout de même recommandé d’ajouter des vérifications pour gérer cette situation.

WordPress inclut un système robuste de rôles et capacités des utilisateurs, qui vous permet soit d’utiliser les rôles et capacités par défaut, soit d’en créer des personnalisés.

Dans ce cas, il pourrait suffire d’autoriser uniquement les utilisateurs disposant de la capacité de gérer les options du site, une capacité standard incluse dans le rôle d’administrateur.

function wp_learn_delete_form_submission() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }
    // rest of function code
}

La documentation du développeur WordPress contient une section détaillée sur les rôles et capacités des utilisateurs, qui comprend une liste des capacités par défaut ainsi que des informations sur la création de capacités personnalisées.

Pour aller plus loin

Pour vous préparer à développer en gardant la sécurité à l’esprit, veillez à lire l’article de la documentation du développeur WordPress consacré à la sécurité. Il contient tous les exemples de cette leçon, ainsi que des informations supplémentaires sur les bonnes pratiques de sécurité, les vulnérabilités courantes et d’autres exemples de code.