Sécurité

Principe général

Never trust user input

Toutes les informations provenant de l'utilisateur doivent être contrôlées

Exemple

Vous trouverez un code d'exemple dans le dossier never_trust_user_input de l'archive.

Voyez-vous un problème de sécurité dans ce code?

Supposons qu'un utilisateur achète des articles sur un site de e-commerce. À la fin, on lui propose de donner de l'argent à une oeuvre de charité avec ce formulaire:

<form method="post">
    Souhaitez vous, en plus de votre achat, donner de l'argent
    à une oeuvre de charité?
    <select name="charityPrice">
        <option value="0">Non merci</option>
        <option value="10">10 €</option>
        <option value="50">50 €</option>
        <option value="100">100 €</option>
    </select>
    <input type="submit" />
</form>

Et que, côté serveur, on calcule le prix final qu'il devra payer:

<?php
// ...
$totalPrice = $basketPrice;
if ($_POST['charityPrice']) {
    $totalPrice += $_POST['charityPrice'];
}

Ici:

  • Si on n'écrit pas explicitement de code pour le faire, rien ne vérifie que la valeur choisie par l'utilisateur était une des valeurs proposée du menu <select>
  • Et si l'utilisateur forgeait une requête dans laquelle charityPrice était un nombre négatif? On se retrouverait alors à soustraire du prix total!

Ici, le problème peut être simplement corrigé en vérifiant que l'utilisateur fournit bien une des valeurs proposées côté serveur (en PHP).

En d'autre termes, ne pas faire confiance aux données POST provenant de l'utilisateur.

Injection SQL

Un code d'exemple se trouve dans le dossier sql_injection de l'archive. Vous aurez besoin d'éditer pdo.php pour vous connecter à la base de données shows.

Voyez-vous un problème ?

Si vous vous souvenez de ce qui a été dit en cours, le problème provient de cette ligne:

<?php
$query = $pdo->query('SELECT title, plot FROM series WHERE id='.$_GET['id']);

Il ne faut jamais faire ça!

Le problème avec ce code est qu'il permet à l'utilisateur d'altérer également la structure de la requête elle même.

Un exemple d'exploitation ici serai de fournir la valeur suivante au paramètre GET id:

show.php?id=99999999 UNION SELECT email, password FROM user LIMIT 1

La requête (réindentée) devient alors:

SELECT title, plot
FROM series
WHERE id=99999999
UNION
SELECT email, password
FROM user
LIMIT 1

Ici:

  • On choisit un identifiant 99999999 qui n'existe probablement pas
  • On créé une union avec la table user dans laquelle on récupère l'email et le mot de passe d'un utilisateur

Cette attaque nécessite de trouver le nom des tables, des champs etc. Mais ce travail est automatisable!

Pour éviter cette faille, il suffit d'utiliser une requête préparée.

XSS et CSRF

Regardez à présent le code de xss_csrf, qui sera utilisé pour cette partie et la suivante. Il faudra également configurer pdo.php, et s'assurer d'avoir un utilisateur en base qui a un identifiant décrit par $userId dans index.php.

Voyez-vous quelque chose de dangereux?

XSS

La première faille ici est une faille de type XSS (Cross-Site Scripting). Le problème est que le contenu des commentaires n'est pas "échappé" lorsqu'ils sont affichés dans la page HTML.

Par exemple, si vous saisissez un commentaire du type:

<b>Bonjour</b>!

Le texte apparaitra en gras.

Le problème, c'est que cela vous permet également d'entrer un commentaire du type:

<script>document.location='https://google.com';</script>

Désormais, la page vous redirigera vers https://google.com!

Cette faille permet de faire des choses bien pires, dont notamment voler le cookie de session, mais vous avez déjà compris le principe.

Pour corriger cette faille, il est nécessaire d'échapper les données de la base de données au moment de l'affichage, par exemple à l'aide de la fonction htmlspecialchars().

Lorsque vous utilisez un moteur de template comme Twig, les valeurs sont échappées par défaut. C'est à dire que c'est comme si htmlspecialchars() était autour de tous vos affichages. Si vous voulez réellement afficher du HTML, vous pourrez utiliser le filtre raw.

CSRF

Il subsiste une deuxième faille à laquelle il est bien plus dur de penser sur cette page.

Imaginez que quelqu'un construise ce formulaire:

<form method="post" action="http://votre-site-web.com/index.php">
<textarea rows="5" cols="100" name="comment">J'adore cette série!</textarea>
<br/>
<input type="submit" value="Envoyer" />
</form>

Où l'URL dans l'attribut action désignerait votre site web en production.

On pourrait même agrémenter ce formulaire d'une soumission automatique déclenchée en JavaScript au chargement de la page:

<body onload="post()">
<script type="text/javascript">
function post() {
    document.getElementById('form').submit();
}
</script>

Vous trouverez cet exemple dans exploit.html

Un tel formulaire peut être hébérgé n'importe où, par exemple sur un site web géré par l'attaquant, qui pourrait alors vous forcer à soumettre des données arbitraires vers le formulaire en question.

Le problème, c'est qu'en soumettant le formulaire, vous utiliserez également votre cookie de session et serez reconnus par le serveur!

Corriger cette faille est un peu plus technique, et se fait en plusieurs étapes:

  • Ajouter un champ hidden dans le formulaire, que l'on appelle généralement csrf_token (jeton CSRF)
  • Le contenu de ce champ est généré au hasard et stocké également dans la session
  • Quand le formulaire est soumis, on contrôle que la valeur du jeton CSRF correspond à celle stockée dans la session

Ce travail est automatiquement assuré par un framework web.

Mots de passes et hachage

Regardez le code source du dossier password_hash.

Est-ce quelque chose vous choque?

Ici, le mot de passe super_password est stocké en clair dans le fichier:

if ($_POST['password'] === 'super_password') {

Supposez que le serveur soit compromis (imaginons qu'une autre faille permette à quelqu'un d'avoir un accès en lecture aux données). Il connaîtra alors le mot de passe.

Le même principe s'applique si le mot de passe est stocké dans la base de données.

Un mot de passe ne doit jamais être stocké en clair!

Au lieu de ça, on utilise une fonction de hachage (hash en anglais), ou d'empreinte, comme par exemple les fonction password_hash() et password_verify() en PHP.