web app security fix

This commit is contained in:
Thanakorn S
2026-05-11 13:49:44 +07:00
parent d989e59edc
commit 7a87909392
7 changed files with 107 additions and 21 deletions
+44 -9
View File
@@ -70,14 +70,37 @@ $user_id = $sth->fetchColumn();
$username = strtolower($data["username"]);
// ── Step 2: Fetch the user's hashed password ──────────────────────────────────
$sth = $pdo1->prepare("select password from user where username = ? or email = ? limit 1;");
// ── Step 2: Fetch the user's hashed password + lockout state ─────────────────
$sth = $pdo1->prepare("SELECT password, login_attempts, locked_until FROM user WHERE username = ? OR email = ? LIMIT 1;");
$sth->execute(array($username, $username));
$temp = $sth->fetch(PDO::FETCH_ASSOC);
// ── Step 2a: Lockout check — only when the username resolves to a real user ──
// We only block here when $user_id is set (valid username) to avoid leaking
// whether an account exists via a different error message.
if ($user_id && !empty($temp['locked_until'])) {
if (strtotime($temp['locked_until']) > time()) {
// Still within the lockout window — reject
$retry_at = date('H:i', strtotime($temp['locked_until']));
$answer['message'] = "Too many failed attempts. Please try again after {$retry_at}.";
exit(json_encode($answer));
} else {
// Lockout has expired — reset counter so they get a fresh 10 attempts
$pdo1->prepare("UPDATE user SET login_attempts = 0, locked_until = NULL WHERE user_id = :id")
->execute([':id' => $user_id]);
$temp['login_attempts'] = 0;
}
}
// ── Step 3–4: Verify password — exit with error on mismatch ──────────────────
if (password_verify(trim($data["password"]), $temp["password"])) {
// ── Reset lockout on successful password verification ─────────────────────
if ($user_id) {
$pdo1->prepare("UPDATE user SET login_attempts = 0, locked_until = NULL WHERE user_id = :id")
->execute([':id' => $user_id]);
}
// ── Step 5a: Fetch full user record ──────────────────────────────────────
// 'support' user gets a hardcoded email so it can always log in even without
// a registered email address in the DB.
@@ -223,15 +246,10 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
// ── Step 5f: Licence expiry check ────────────────────────────────────────
// $expire is loaded from db_auth.php via session/preset bootstrap.
// If the licence expired more than 1 day ago, reject the login.
// Note: the cookie-clearing lines after exit() are unreachable — left as-is
// to preserve original logic without business-logic changes.
if (strtotime("now") > strtotime($expire . " + 1 day")) {
session_destroy();
$answer["expire"] = "expire";
exit(json_encode($answer));
setcookie("u", "", time() - 1, "/"); // unreachable — preserved from original
setcookie("h1", "", time() - 1, "/");
setcookie("h2", "", time() - 1, "/");
}
// ── Step 6: Generate 6-digit TOTP ────────────────────────────────────────
@@ -327,8 +345,25 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
} else {
// ── Password mismatch ─────────────────────────────────────────────────────
// Clear identifying cookies on failure to prevent cookie-based session reuse.
$answer["message"] = "Incorrect Password";
// Only increment the counter when the username is valid — wrong usernames
// don't count so a typo in your own name doesn't eat your own attempts.
if ($user_id) {
$attempts = (int)($temp['login_attempts'] ?? 0) + 1;
if ($attempts >= 10) {
$locked_until = date('Y-m-d H:i:s', strtotime('+30 minutes'));
$pdo1->prepare("UPDATE user SET login_attempts = :a, locked_until = :l WHERE user_id = :id")
->execute([':a' => $attempts, ':l' => $locked_until, ':id' => $user_id]);
$retry_at = date('H:i', strtotime($locked_until));
$answer['message'] = "Too many failed attempts. Please try again after {$retry_at}.";
} else {
$pdo1->prepare("UPDATE user SET login_attempts = :a WHERE user_id = :id")
->execute([':a' => $attempts, ':id' => $user_id]);
$answer['message'] = "Incorrect Password";
}
} else {
$answer['message'] = "Incorrect Password";
}
setcookie("u", "", time() - 1, "/");
setcookie("h1", "", time() - 1, "/");
setcookie("h2", "", time() - 1, "/");