Développement d'applications Web avec LAMP

Document d'accompagnement pour le cours 420-KB9-LG

Éléments de sécurité

Dernière mise à jour le 10 novembre 2022

Ce document rassenble quelques informations de base pour assurer la sécurité de nos sites Web et les données qui s'y trouvent.

Injections SQL

Une injection SQL consiste à injecter dans la requête SQL en cours un extrait de requête non prévu par le système et pouvant en compromettre la sécurité.

Voyons quelques exemples...

Commençons par créer une table :

CREATE TABLE etudiant (
  id INT NOT NULL,
  nom VARCHAR(50),
  PRIMARY KEY (id)
);

INSERT INTO etudiant (id, nom) values (248356458, 'Alain Patoche');
INSERT INTO etudiant (id, nom) values (330396283, 'Greta Tremblay');
INSERT INTO etudiant (id, nom) values (760552230, 'Méo Gagnon');
INSERT INTO etudiant (id, nom) values (270570976, 'Linda Monette');
INSERT INTO etudiant (id, nom) values (421576623, 'Robert Gratton');

Injections basées sur une condition toujours vraie

C'est un type très connu d'injection SQL. Il s'agit d'ajouter à la clause WHERE de la requête le code nécessaire pour que toutes les rangées d'une table soient retournées.

Si on entre des données normalement dans le formulaire :

Code de l'étudiant : 248356458

Tout va bien. Voici la requête générée et son résultat (telle qu'ils seraient vus dans la console MySQL) :

MariaDB [exemple]> SELECT * FROM etudiant WHERE id = 248356458;
+-----------+---------------+
| id        | nom           |
+-----------+---------------+
| 248356458 | Alain Patoche |
+-----------+---------------+
1 row in set (0.001 sec)

MariaDB [exemple]> 

Mais en ajoutant un petit quelque chose à la fin de la requête il est faicle d'obtenir tout le contenu de la table :

Code de l'étudiant : 100000000 OR 1 = 1

Ce qui génère la requête suivante :

MariaDB [exemple]> SELECT * FROM etudiant WHERE id=100000000 OR 1 = 1;
+-----------+----------------+
| id        | nom            |
+-----------+----------------+
| 248356458 | Alain Patoche  |
| 270570976 | Linda Monette  |
| 330396283 | Greta Tremblay |
| 421576623 | Robert Gratton |
| 760552230 | Méo Gagnon     |
+-----------+----------------+
5 rows in set (0.003 sec)

MariaDB [exemple]> 

Injections basées sur un lot de requêtes

Ici la méthode consiste à enchaîner plusieurs requêtes SQL :

Code de l'étudiant : 100000000; DROP TABLE etudiant

Les conséquences peuvent être désastreuses.

MariaDB [exemple]> SELECT * FROM etudiant WHERE id=100000000; DROP TABLE etudiant;
Empty set (0.002 sec)

Query OK, 0 rows affected (0.012 sec)

MariaDB [exemple]> show tables;
Empty set (0.001 sec)

MariaDB [exemple]> 
Nooon!!!
Nooon!!!

Injections basées sur les commentaires SQL

Une autre méthode (il y en a d'autres, mais nous nous arrêterons ici) consiste à ajouter un symbole de commentaire (--) pour briser la requête finale.

Supposons la requête suivante (les deux valeurs étant des paramètres de formulaire) :

  SELECT id FROM usager WHERE pseudo = '...' AND mdp = '...';

D'accord c'est un peu simpliste, mais c'est juste un exemple.

Un utilisateur malveillant pourrait entrer :

Pseudo : bob';--

Mot de passe : allo <-- n'importe quoi ici

La requête générée sera :

  SELECT id FROM usager WHERE pseudo = 'Bob';--' AND mdp = 'allo';

Ce qui revient à :

  SELECT id FROM usager WHERE pseudo = 'Bob';

Plus besoin de mot de passe!

Quoi faire?

Les deux meilleurs solutions pour éviter les injectios SQL consistent à utiliser des procédures stockées ou des requêtes préparées. Dans les deux cas des paramètres et non des chaînes de caractères sont utilisées dans les requêtes à la base de données.

Mais comment les requêtes préparées peuvent-elles nous protéger contre les injections SQL?

Dans le fond tout est très simple. Une requête SQL traditionnelle permet à un utilisateur malveillant d'ajouter aux données du code SQL. Ainsi, là où on s'attend à avoir seulement des données ("100000000"), on peut retrouver un mélange de données et de code SQL ("100000000; DROP TABLE etudiant").

Dans le cas d'une requête préparée, le code de la requête comme tel est d'abord envoyé au serveur où il est précompilé. Les données sont ensuite envoyées sous forme de paramètres comme dans n'importe quel programme.

Imaginez un instant une méthode (langage Java) comme celle-ci :

public void faireQuelqueChose(string chaine, int nombre) {
  ...
}

Que pensez-vous qu'il va arriver si on appelle la méthode ainsi :

faireQuelqueChose("for (int i = 0; i < tab.length; i++) { tab[i] = 0; }", 13);

Rien du tout! En fait, on ne sait pas exactement comment va réagir la méthode, mais ce qui est certain est que le code passé dans le premier paramètre ne sera jamais exécuté.

Les requêtes préparées fonctionnent exactement de la même façon. Voilà pourquoi elles sont plus sûres.

Boromir
Des paroles sages de Boromir

Chiffrement du mot de passe

AVERTISSEMENT - La cryptographie est un discipline complexe qui fait appel à plusieurs concepts qu'il serait long de présenter ici. Nous nous concentrerons donc sur ce qui est important pour assurer la sécurité des mots de passe et laisseront faire le reste.

Importance de chiffrer les mots de passe

On ne doit jamais conserver sur un serveur des mots de passe non chiffrés (codés, transformés par un procédé de chiffrement).

Si un serveur est piraté et que des mots de passe non chiffrés sont volés, les conséquences légales pour les responsables du site Web peuvent être importantes.

Il existe en PHP une manière très simple de gérer le chiffrement et la vérification des mots de passe, c'est le duo de fonctions password_hash() et password_verify().

La fonction password_hash()

Cette fonction crée une version chiffrée d'un mot de passe (plus rigoureusement une clé de hachage) en utilisant un algorithme irréversible. Il est donc impossible pour un pirate, malgré ce que nous montre le cinéma, d'obtenir le mot de passe à partir de sa valeur chiffrée (remarquez que les données obtenues pourront quand même l'aider à long terme pour des attaques par dictionnaire).

Le premier paramètre de la fonction est le mot de passe originel. Le deuxième paramètre est une constante qui indique l'algorithme de chiffrement à utiliser.

Exemple :

$mdp = "citrouille";

$hash = password_hash($mdp, PASSWORD_DEFAULT);

echo $hash;

Sortie :

$2y$10$VbSjlZ0D8GU5dCiYcMr.Au1SqRa5knNUIgoSWck0yYkUfOrBSAxzy

Il est recommandé d'utiliser la constante PASSWORD_DEFAULT qui indique l'algorithme le plus fort et le plus pratique disponible en ce moment.

La clé (mot de passe chiffrée) aura une longueur de 60 caractères. Pensez-y lorsque viendra le temps de concevoir votre base de données!

La fonction password_verify()

Générer une version chiffrée d'un mot de passe est donc une chose facile. Mais quelle est donc la meilleure façon de valider un mot de passe fourni par un utilisateur?

On pourrait penser qu'il faille appeler explicitement la fonction password_hash() avec comme paramètre le mot de passe fourni par l'utilisateur et comparer ensuite avec la valeur contenue dans la base de donnée, mais PHP a pensé à nous avec la fonction password_verify().

Cette fonction prend en paramètre deux chaînes de caractères et vérifie si la seconde est bien la version chiffrée de la première.

Exemple :

// modifiez un caractère de cette chaîne pour voir le résultat
$hash = '$2y$10$VbSjlZ0D8GU5dCiYcMr.Au1SqRa5knNUIgoSWck0yYkUfOrBSAxzy';

if (password_verify("citrouille", $hash)) {
  echo "valide";
} else {
  echo "invalide";
}

Faut-il redémarrer le système lorsque demandé?

Quelques temps après l'installation de votre VPS, un message a commencé à s'afficher dans la console à chaque connexion à l'aide de votre client SSH (ex : PuTTY). C'est le fameux message "System Restart Required" :

New release '22.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.

*** System restart required ***
Last login: Tue Nov 22 00:53:10 2022 from 76.10.131.23

Mais pourquoi me demande-t-on de redémarrer le système d'exploitation?

C'est généralement parce qu'une mise à jour du noyau Linux a été installée. Et puisque ces mises à jour ne prennent effet qu'après un redémarrage et qu'il s'agit souvent de mises à jour de sécurité, il est important de ne pas trop attendre et de relancer le système dès que possible.

On effectue un redémarrage de la façon suivante :

$  sudo reboot

L'opération prend de quelques secondes à quelques minutes selon l'ordinateur.

Vous n'avez pas à redémarrer immédiatement en présence du message ou à vérifier chaque jour si cela vous est demandé. Vous devez toutefois être conscient que, pendant ce temps-là, votre système est vulnérable aux problèmes de sécurité que la mise à jour a pour but de régler.

Si vous êtes curieux, la commande suivante permet de voir les paquets logiciels (packages) qui demandent le redémarrage :

$  cat /var/run/reboot-required.pkgs
linux-image-5.4.0-132-generic
linux-base
$

Vous devez bien sûr exécuter cette commande avant le redémarrage!