Accueil >PHP >PHP et bases de données >Sessions et connexion sécurisées

Sessions et connexion sécurisées

Après la connexion, la session maintient l’utilisateur connecté au site – et quiconque obtient son identifiant est connecté à sa place. Comment régler le cookie de session, que faire à la connexion et à la déconnexion et comment limiter les tentatives pour deviner les mots de passe.

Les exemples utilisent la connexion $pdo de l’article sur PDO et la table users de l’article sur le stockage des mots de passe.

Réglages du cookie de session

session_set_cookie_params([
  "lifetime" => 0,
  "path" => "/",
  "secure" => true,
  "httponly" => true,
  "samesite" => "Lax",
]);
ini_set("session.use_strict_mode", "1");
session_start();
  • httponly – JavaScript ne peut pas lire le cookie. Une éventuelle faille XSS ne permet donc pas de voler l’identifiant de session.
  • secure – le cookie n’est envoyé que par HTTPS. Tout le site doit donc fonctionner en HTTPS.
  • samesite – la valeur Lax fait que le cookie n’est pas envoyé lors des requêtes POST venant d’autres sites, ce qui est une protection supplémentaire contre le CSRF. La valeur Strict n’enverrait pas le cookie même lorsque l’utilisateur arrive par un lien depuis un autre site, et après un clic sur le lien, il serait apparemment déconnecté.
  • lifetime 0 – le cookie est valable jusqu’à la fermeture du navigateur.
  • use_strict_mode – PHP n’accepte pas un identifiant de session qu’il n’a pas créé lui-même. Un attaquant ne peut donc pas imposer à l’utilisateur un identifiant connu à l’avance.

La transmission des réglages sous forme de tableau fonctionne depuis PHP 7.3. Ils doivent être définis avant session_start et avant toute sortie de la page.

Connexion

function log_in(PDO $pdo, string $email, string $password): bool
  {
    $stmt = $pdo->prepare("SELECT id, password FROM users WHERE email = ?");
    $stmt->execute([$email]);
    $user = $stmt->fetch();
    if(!$user || !password_verify($password, $user["password"])) return false;
    session_regenerate_id(true);
    $_SESSION["user_id"] = (int)$user["id"];
    return true;
  }

session_regenerate_id(true) attribue après la connexion un nouvel identifiant de session et rejette l’ancien. Même si un attaquant connaissait l’identifiant d’avant la connexion, celui-ci est désormais inutilisable. Procédez de même lors d’un changement de droits. N’enregistrez dans la session que l’ID de l’utilisateur ; chargez ses données et ses droits depuis la base de données.

Déconnexion

$_SESSION = [];
$params = session_get_cookie_params();
setcookie(session_name(), "", [
  "expires" => time() - 3600,
  "path" => $params["path"],
  "domain" => $params["domain"],
  "secure" => $params["secure"],
  "httponly" => $params["httponly"],
  "samesite" => $params["samesite"],
]);
session_destroy();

La déconnexion supprime les données de session sur le serveur et le cookie dans le navigateur. Placez le bouton de déconnexion dans un formulaire envoyé par POST avec un jeton CSRF.

Limiter les tentatives de connexion

Sans limite, un attaquant peut deviner des mots de passe automatiquement, des milliers par minute. Les tentatives échouées sont donc enregistrées dans une table :

CREATE TABLE login_attempts (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  ip VARCHAR(45) NOT NULL,
  attempted_at DATETIME NOT NULL,
  KEY idx_ip_attempted_at (ip, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Après cinq tentatives échouées depuis une même adresse IP en 15 minutes, les connexions suivantes sont refusées :

function too_many_attempts(PDO $pdo, string $ip): bool
  {
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at > NOW() - INTERVAL 15 MINUTE");
    $stmt->execute([$ip]);
    return (int)$stmt->fetchColumn() >= 5;
  }
function log_failed_attempt(PDO $pdo, string $ip): void
  {
    $stmt = $pdo->prepare("INSERT INTO login_attempts (ip, attempted_at) VALUES (?, NOW())");
    $stmt->execute([$ip]);
  }
// traitement du formulaire de connexion (après vérification du jeton CSRF)
$email = is_string($_POST["email"] ?? null) ? trim($_POST["email"]) : "";
$password = is_string($_POST["password"] ?? null) ? $_POST["password"] : "";
$ip = $_SERVER["REMOTE_ADDR"];
if(too_many_attempts($pdo, $ip))
  {
    $error = "Trop de tentatives. Réessayez dans 15 minutes.";
  }
elseif(log_in($pdo, $email, $password))
  {
    header("Location: /admin/");
    exit;
  }
else
  {
    log_failed_attempt($pdo, $ip);
    $error = "E-mail ou mot de passe incorrect.";
  }
  • Seuls l’adresse IP et l’heure sont enregistrés. L’e-mail et le mot de passe saisis ne sont enregistrés nulle part.
  • La colonne ip fait 45 caractères pour qu’une adresse IPv6 y tienne aussi.
  • Supprimez régulièrement les anciennes entrées, par exemple une fois par jour : DELETE FROM login_attempts WHERE attempted_at < NOW() - INTERVAL 1 DAY.
  • Si le site fonctionne derrière un reverse proxy ou un CDN, REMOTE_ADDR contient l’adresse du proxy et la limite s’appliquerait à tous les visiteurs à la fois. Le proxy transmet alors l’adresse réelle dans un en-tête – vous ne pouvez vous y fier que si la requête est vraiment passée par votre proxy.

Autres règles

  • Faites fonctionner tout le site en HTTPS et configurez la redirection depuis HTTP sur le serveur.
  • Enregistrez les mots de passe avec password_hash, protégez les formulaires avec un jeton CSRF et n’accédez à la base de données que par des requêtes préparées.
  • Vous pouvez protéger l’administration par une couche supplémentaire, par exemple une restriction d’accès selon l’adresse IP.

Articles connexes :
Stocker les mots de passe avec password_hash
Protéger les formulaires contre le CSRF
Requêtes préparées avec PDO