diff --git a/lib/head.php b/lib/head.php index f37e332..3ae8b67 100644 --- a/lib/head.php +++ b/lib/head.php @@ -1,4 +1,7 @@ - + @@ -9,6 +12,7 @@ + + diff --git a/reset-password.php b/reset-password.php index a5663a4..d5e0a34 100644 --- a/reset-password.php +++ b/reset-password.php @@ -30,19 +30,31 @@ if ($tokenIsWellFormed && $_SERVER['REQUEST_METHOD'] === 'POST') { if (empty($errors)) { $pdo = getDbConnection(); - $userId = consumePasswordResetTicket($pdo, $token); - if ($userId === null) { - $errors['password'] = 'This reset link has already been used or has expired. Please request a new one.'; - } else { - $hashedPassword = password_hash($password, PASSWORD_DEFAULT); - // Bumping session_version invalidates every existing session for this user - // (including a stolen one) the next time enforce_session_version() runs. - $stmt = $pdo->prepare('UPDATE users SET password = :password, session_version = session_version + 1 WHERE id = :id'); - $stmt->execute(['password' => $hashedPassword, 'id' => $userId]); + // Consuming the ticket and updating the password happen in one transaction so a + // failure between the two can't strand the user: either both take effect, or the + // ticket stays unconsumed and the (still valid) reset link can be used again. + $pdo->beginTransaction(); + try { + $userId = consumePasswordResetTicket($pdo, $token); - header('Location: login.php?reset=success'); - exit; + if ($userId === null) { + $pdo->rollBack(); + $errors['password'] = 'This reset link has already been used or has expired. Please request a new one.'; + } else { + $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + // Bumping session_version invalidates every existing session for this user + // (including a stolen one) the next time enforce_session_version() runs. + $stmt = $pdo->prepare('UPDATE users SET password = :password, session_version = session_version + 1 WHERE id = :id'); + $stmt->execute(['password' => $hashedPassword, 'id' => $userId]); + $pdo->commit(); + + header('Location: login.php?reset=success'); + exit; + } + } catch (Throwable $e) { + $pdo->rollBack(); + throw $e; } } }