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.
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>'; |
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é!