Sécuriser son jeu web

Monitorer son jeu

La surveillance en continue de votre jeu web (nombre de pages vues, de hits 4xx/5xx, d'inscriptions, le nombre de messages échangés, contenu de ces échanges sur le forum, etc) est essentielle, pour pouvoir réagir avant que les choses ne dégénèrent trop.

Le spam

Les captchas ne sont pas un bon moyen de lutter contre le spam, car les spammeurs humains existent (oui, on peut payer des sommes ridicules à des gens dans des pays sous-développés pour qu'ils résolvent en boucle les captchas histoire de poster des spams…!) et car les robots peuvent parfois être utiles (les robots d'indexation, vos clients d'APIs, etc).

Privilégiez l'analyse de contenu, pour être capable de déterminer si l'action (le message à poster par exemple) est légitime et autorisée ou non, qu'importe que cette action vienne d'un robot ou d'un humain. Ce qui compte, c'est la qualité de l'action (l'intérêt du message) plus que l'auteur de l'action (du message).

"Prod" et "Dev"

N'éditez pas les sources de votre jeu en ligne (via FileZilla par exemple), car votre jeu sera inconsistant: dès l'instant où vous aurez besoin d'éditer 2 fichiers, vous ne pourrez pas les sauver en même temps, et donc, un joueur pourrait se retrouver "entre deux sauvegarde", et le jeu plantera pour lui. De plus, vous n'aurez aucun moyen d'annuler une modification si vous vous plantez, et enfin, aucun moyen de tester et de débugger vos modifications.

Vous devez créer une version "locale" de votre jeu, sur votre propre PC via WAMP, faire vos tests et debug là dessus, et uniquement une fois le jeu "prêt", le mettre en ligne.

Entropie d'un mot de passe

L'entropie d'un mot de passe secret aléatoirement généré est le nombre de symboles possibles, élevé à la puissance de la longueur du secret. Ainsi, un mot de passe de 8 chiffres aléatoires a une entropie de 10^8. Un mot de passe de 16 caractères alphanumériques minuscules/majuscules (62 possibilités) a une entropie de 62^16 soit environ 10^28. Une "passphrase" de 4 mots choisis au hasard parmi les 2000 mots du langage courant a une entropie de 2000^4 soit environ 10^13.

L'entropie d'un secret doit dépasser la capacité de brute force de l'attaquant et son temps disponible. Comme un PC classique peut faire jusqu'à 10 milliards de hashs/seconde, cela fait 10^19 essais sur toute une vie, soit l'équivalent de 11 caractères alphanumériques aléatoires (avec une grosse marge, disons 16 caractères alphanumériques: il faudrait 1 milliard de PCs à l'attaquant!).

Une passphrase de 4 mots parmis les 2000 courants est moins sûre qu'un mot de passe aléatoire de 8 caractères alphanumériques (minuscules et majuscules). Pour égaler un mot de passe de 16 caractères aléatoires (caractères spéciaux inclus soit ~86 possibilités), il vous faudrait une passphrase de… 7 mots, en les prenant au hasard dans tout le dictionnaire (environ 40000 mots, ce qui inclut les mots banderille demiurge labié quart-monde wigwam…)!
Donc, les passphrases ne sont pas sécurisées; oubliez-les.

Passé les 62 possibilités de l'alphanumérique, ce n'est pas en augmentant le nombre d'éléments possibles (= la taille du dictionnaire) que vous rendrez votre secret plus sûr, mais en augmentant sa longueur.

Stocker les secrets

Pour stocker un secret (mots de passe des joueurs, ids de sessions, tokens de réinitialisation des mots de passe, etc) utilisez uniquement password_hash et password_verify .

Ne stockez pas les mots de passe en clair (si votre serveur a besoin d'un mot de passe, alors cryptez-le au moins avec une clef stockée dans l'application et non dans la BDD), ne les salez/hashez pas vous même.

$pdo->prepare('INSERT INTO comptes (nom, mot_de_passe) VALUES (?, ?)') ->execute(array($playerName, password_hash($playerPassword))); $hashedPassword = $pdo->prepare('SELECT * FROM comptes WHERE nom = ?') ->execute(array($playerName)); if (password_verify($playerPassword, $hashedPassword['mot_de_passe'])) { // OK } else { echo "Mauvais login ou mauvais mot de passe"; }
La bonne façon de stocker (inscription) et vérifier (connexion) le mot de passe de vos joueurs

Un pirate qui pourrait lire toute votre base de données ne doit pas être capable de voler le compte d'un joueur.

Vérifier côté serveur, aider côté client

Un hacker peut modifier son navigateur (ou plus simplement, la page web affichée) pour retirer les restrictions sur les input. Il peut même ne pas être passé par votre formulaire! Vous devez donc mettre des aides côté client (les attributs sur les input textarea select etc) et ajouter également des vérification de sécurité côté serveur.

N'oubliez pas de vérifier non seulement le type des données (exemple: le champ id=… doit être un nombre entier positif inférieur à deux milliards), mais également les droits d'accès du joueur (si l'id est l'id d'un batiment à détruire, alors vérifiez aussi que le joueur possède ce batiment, et qu'il n'est pas en train de détruire le batiment d'un autre joueur).

Puisqu'un hacker peut modifier la page web affichée sur son navigateur, il peut aussi modifier le client lourd (Java, exécutable, appli mobile, etc) sur sa machine. Il est donc impossible de protéger un "client lourd", comme le démontre mon exemple de crackage d'une application exécutable (.exe) via OllyDbg .

Injections (SQL, XSS, etc)

Si vous concaténez des chaines de caractères, alors ne les exécutez pas (dans une query, dans le navigateur, dans un JS, etc).

Faille
de sécurité
Mauvais exemple Bonne solution
SQL INJECTION! $playerInfos = $pdo->query("SELECT * FROM players WHERE id = " . $_GET['idPlayer']); // SQL INJECTION (meme si ce sont des "prepared statement": elles sont mal utilisées)! $statement = $pdo->prepare("SELECT * FROM players WHERE id = " . $_GET['idPlayer']); $playerInfos = $statement->execute(); $statement = $pdo->prepare("SELECT * FROM players WHERE id = ?"); $playerInfos = $statement->execute(array($_GET['idPlayer']));
XSS (reflected) echo $_GET['idPlayer']; echo htmlentities($_GET['idPlayer']);
XSS (stored) echo $pdo->query("SELECT pseudo FROM players WHERE id = 1")[0]['pseudo']; echo htmlentities($pdo->query("SELECT pseudo FROM players WHERE id = 1")[0]['pseudo']);
XSS (via JS) <script> const x = <?php echo $_GET['idPlayer']; ?>; console.log(x); </script> <script> var x = <?php echo json_encode($_GET['idPlayer']); ?>; console.log(x); </script>
JS générant un XSS document.querySelector('code').innerHTML = window.location.href; document.querySelector('code').innerText = window.location.href;
Command Line Injection! exec("ffmpeg -i \"" . $_GET['idPlayer'] . "\""); // Dans tous les cas, ne pas utiliser "exec" ou "passthru" sera plus sûr encore! exec("ffmpeg -i \"" . escapeshellarg($_GET['idPlayer']) . "\"");
Protocol injection echo '<a href="' . htmlentities($_GET['idPlayer']) . '">cliquez ici</a>'; // ATTENTION cet exemple est encore vulnerable (cf ci-dessous) echo '<a href="' . htmlentities('/?id=' . $_GET['idPlayer']) . '">cliquez ici</a>';
URL injection echo '<a href="' . htmlentities('index.php?id=' . $_GET['idPlayer']) . '">cliquez ici</a>'; echo '<a href="' . htmlentities('index.php?id=' . urlencode($_GET['idPlayer'])) . '">cliquez ici</a>';
Ces codes ne sont pas sécurisés! Ils sont injectables!

Pensez aussi à réduire les droits de l'utilisateur SQL de votre jeu web au strict minimum: votre user SQL ne doit pas avoir le droit de faire un DROP TABLE par exemple.

CSRF

N'utilisez GET que si la page ne fait aucune modification côté serveur (mis à part des entrées de log par exemple). Pour tout ce qui doit modifier les données côté serveur (attaque d'un autre joueur, déplacement d'une unité, construction d'un bâtiment, etc), la requête doit être reçue par le serveur en POST uniquement. Le serveur doit rejeter toute autre requête faite en GET.
De plus, dans le cadre d'une requête POST, vérifiez aussi les header HTTP Origin ou Referer pour vous assurer que le joueur était bien sur le site du jeu lorsqu'il a envoyé la requête.

Identifier un élément de jeu

Identifiez les éléments de jeu par des id numériques plutôt que des chaînes string, voir (pire!) des labels "human-redable". attack.php?pseudoJoueur=Xenos n'est pas une bonne idée, car vous devrez gérer toutes les valeurs possibles des pseudos (y compris les caractères UTF8 "Right-To-Left" et autres zero-width space), alors que attack.php?idJoueur=4 sera plus facilement gérable.

Du javascript dynamique

Evitez de balancer du code PHP dans votre javascript: cela vous fait prendre des risques énormes en terme de sécurité et d'injection. Donc, évitez les var x = <?php echo json_encode($x); ?>; et privilégiez le fait de sortir les données dans la page HTML, et de garder un javascript static, comme <span data-x="<?php echo htmlentities($x); ?>"> avec var x = document.querySelector('span[data-x]').dataset.x;

Si malgré tout, vous faite un echo dans un code javascript, alors inutile de faire en plus un htmlentities car la spécification HTML dit que le contenu d'une balise script n'est pas interprété comme du HTML, mais comme un texte brut.

Obfuscation inutile

Obfusquer votre code (Java, CSS, JS, HTML, etc) n'est pas utile, car il sera toujours possible de le remettre d'aplomb. Voire même, ce sera totalement inutile: si un code obfusqué complexe fait une requête web, je vais juste mettre un proxy en place pour voir directement quelle est cette requête, plutôt que de rétro-engineerer votre application!

Minification et obfuscation sont inutiles, et vous poseront plus de soucis que de sécurité!