JeuWeb - Crée ton jeu par navigateur
Article Sécurité: Partage de sessions et mutualisé - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Article Sécurité: Partage de sessions et mutualisé (/showthread.php?tid=8238)



Sécurité: Partage de sessions et mutualisé - Xenos - 15-09-2020

Salutations,

Voici un petit post-article qui concernera surtout les gens qui ont un seul pool de session pour plusieurs applications. C'est le cas, principalement, si:
- Vous avez plusieurs jeux ou un jeu et des sites
- Le tout sur un serveur mutualisé où les sessions sont toutes dans le même sac


Environnement (setup)

Imaginez que vous avez deux jeux, hébergés on va dire sur deux domaines différents riri.reinom.com et loulou.reinom.com (en pratique, s'il s'agit du même domaine, le même problème s'applique). Ces deux jeux sont écrits dans le même langage (on va prendre PHP, mais le concept est applicable quelque soit le langage). Ces deux jeux sont hébergés sur un même serveur, mutualisé (cela marchera aussi sur un même VPS ou un même dédié, dès l'instant où le pool de session est commun aux deux jeux, genre un même dossier sans préfixe pour les deux jeux, aka pour les deux domaines).

Et, pour la simplicité, on va considérer que les deux jeux ont les codes suivants:
// riri.reinom.com
session_start();
if ($_SESSION['is_logged_in']) {
    echo "Bienvenue " . htmlentities($_SESSION['username']);
    echo "Votre argent: " . $orm->getFromDb('SELECT argent FROM player_money WHERE id_player = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

// loulou.reinom.com
session_start();
if ($_SESSION['is_logged_in']) {
    echo "Salut mon loulou!";
    echo "T'étais pas revenu depuis " . $orm->getFromDb('SELECT last_login_date FROM account_infos WHERE id = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

Voyez-vous déjà le soucis? Allez, regardons ce qui va se passer...


L'attaque

Le principe de l'attaque est assez simple: on va utiliser la session du 1er jeu (riri) sur le second (loulou). Pour cela:
  • On va sur le 1er jeu
  • On se connecte, et le jeu nous dit "Bienvenue, Xenos! Votre argent: 1000"
  • On se connecte sur le 2nd jeu, loulou.reinom.com
  • Le jeu nous dit "Salut mon loulou! T'étais pas revenu depuis 2020-09-15 17:05:02"
  • On ouvre sa console, et on trouve un cookie PHPSESSID qui vaut, admettons, 8b9e12af3258ad379c197dbb6642d2c6
  • On revient sur le premier jeu, riri.reinom.com
  • On ouvre la console du navigateur, et on édite ses cookies pour modifier PHPSESSID et lui donner la valeur 8b9e12af3258ad379c197dbb6642d2c6
  • On recharge la page
  • riri.reinom.com nous dit alors "Bienvenue, Machinchose! Votre argent: 5000" car on est connecté sur le compte d'un autre joueur!


Que se passe-t-il?

Quand on se loggue sur le 1er jeu, une session PHP est crée avec l'ID (disons) 1d6a1ca01a5fe10bba249eeb9f72ce22. Elle correspond à la session de Xenos et contient (disons) is_logged_in: true; username: Xenos; id_player: 42.

Maintenant, quand on va sur le second jeu, le même processus se passe: une session, avec l'ID 8b9e12af3258ad379c197dbb6642d2c6 disons, est crée sur le serveur, et le client reçoit le cookie correspondant pour être authentifié. On va dire que, dans cette session, le jeu Loulou aura mis les données is_logged_in: true; username: Machinchose; id_player: 314

Enfin, si on revient sur le 1er jeu, et qu'on change son cookie, pour mettre 8b9e12af3258ad379c197dbb6642d2c6, alors le serveur ira chercher la session avec l'ID 8b9e12af3258ad379c197dbb6642d2c6. Comme le pool de session des deux jeux est commun (c'est le cas sur un serveur mutualisé par exemple, ou sur un serveur dédié dans lequel vous n'avez pas configuré 1 pool par virtual host) alors le jeu "Riri" trouvera bien une session 8b9e12af3258ad379c197dbb6642d2c6 dans le pool, mais cette session a été initialisée par l'autre jeu, Loulou!
Le jeu Riri ouvrira donc la session contenant les données is_logged_in: true; username: Machinchose; id_player: 314 d'où de possibles problèmes!


Correction

Pour éviter cela, plusieurs pistes:


Sur un dédié
Mettez un pool de session par jeu (par vhost, ou par Directory suivant la granularité requise). Vous pouvez utiliser session_save_path pour cela:

// riri.reinom.com
session_save_path('/tmp/game/riri');
session_start();
if ($_SESSION['is_logged_in']) {
    echo "Bienvenue " . htmlentities($_SESSION['username']);
    echo "Votre argent: " . $orm->getFromDb('SELECT argent FROM player_money WHERE id_player = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

// loulou.reinom.com
session_save_path('/tmp/game/loulou');
session_start();
if ($_SESSION['is_logged_in']) {
    echo "Salut mon loulou!";
    echo "T'étais pas revenu depuis " . $orm->getFromDb('SELECT last_login_date FROM account_infos WHERE id = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

Chaque jeu aura alors son propre pool de sessions, et ils ne se marcheront pas dessus.
Note: je n'ai jamais testé cette solution, n'ayant pas de setup correspondant


Sur un mutualisé
On "pourrait" utiliser session_save_path mais je ne le recommande pas, car, d'une part, il est facile de l'oublier/mal le placer, mais surtout, où allez-vous sauver vos sessions? Sur un mutu, ce sera très compliqué de changer ce path, car il est hors de question de stocker ça dans votre espace web (genre /mohe/reinom/riri/tmp à cause des performances mais surtout des droits) et vous n'aurez probablement pas la possibilité de stocker cela à un endroit arbitraire autre que celui par défaut.

Je recommande donc d'ajouter un token à ses sessions:
// riri.reinom.com
$token = 'riri.2020.09.10';

session_start();
if (($_SESSION['token'] ?? '') !== $token) {
    // Reset the session: it's a new one or a cross-domain one
    $_SESSION = array();
}

if ($_SESSION['is_logged_in']) {
    echo "Bienvenue " . htmlentities($_SESSION['username']);
    echo "Votre argent: " . $orm->getFromDb('SELECT argent FROM player_money WHERE id_player = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

// loulou.reinom.com
$token = 'loulou.2020.09.15';

session_start();
if (($_SESSION['token'] ?? '') !== $token) {
    // Reset the session: it's a new one or a cross-domain one
    $_SESSION = array();
}

if ($_SESSION['is_logged_in']) {
    echo "Salut mon loulou!";
    echo "T'étais pas revenu depuis " . $orm->getFromDb('SELECT last_login_date FROM account_infos WHERE id = ?', array($_SESSION['id_player']));
} else {
    echo "Veuillez vous connecter";
}

Avec ce simple "if" et cette variable "token", unique pour chaque jeu, vous serez assuré que la session ne sera pas "partagée" entre deux jeux.

Perso, j'ajoutais (passé, car je n'utilise finalement plus les sessions PHP d'OVH pour une autre raison) la date à ce token. Cela permet, quand on fait une mise à jour qui "casse" la rétrocompatibilité avec les sessions (ie: ajouter/modifier/supprimer une $_SESSION de son code) de changer le token, et d'être sûr que toutes les sessions existantes seront considérées comme "invalides", et réinitialisées.

Voilà pour ce petit article sécu. Il sera ajouté aux topics de "guide" à venir Wink
Si vous avez des questions, n'hésitez pas à répondre au message!