added brute force protection

This commit is contained in:
Ryan
2025-03-22 05:44:56 -04:00
committed by GitHub
parent 2e55f5f4d7
commit edb8ff476a
4 changed files with 94 additions and 29 deletions

View File

@@ -7,7 +7,7 @@ $usersFile = USERS_DIR . USERS_FILE;
// Determine if we are in setup mode: // Determine if we are in setup mode:
// - Query parameter setup=1 is passed // - Query parameter setup=1 is passed
// - And users.txt is either missing or empty // - And users.txt is either missing or empty
$isSetup = (isset($_GET['setup']) && $_GET['setup'] == '1'); $isSetup = (isset($_GET['setup']) && $_GET['setup'] === '1');
if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '')) { if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile)) === '')) {
// Allow initial admin creation without session checks. // Allow initial admin creation without session checks.
$setupMode = true; $setupMode = true;
@@ -16,7 +16,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile))
// In non-setup mode, check CSRF token and require admin privileges. // In non-setup mode, check CSRF token and require admin privileges.
$headers = array_change_key_case(getallheaders(), CASE_LOWER); $headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : ''; $receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) { if (!isset($_SESSION['csrf_token']) || $receivedToken !== $_SESSION['csrf_token']) {
echo json_encode(["error" => "Invalid CSRF token"]); echo json_encode(["error" => "Invalid CSRF token"]);
http_response_code(403); http_response_code(403);
exit; exit;
@@ -30,7 +30,7 @@ if ($isSetup && (!file_exists($usersFile) || trim(file_get_contents($usersFile))
} }
} }
// Get input data from JSON // Get input data from JSON.
$data = json_decode(file_get_contents("php://input"), true); $data = json_decode(file_get_contents("php://input"), true);
$newUsername = trim($data["username"] ?? ""); $newUsername = trim($data["username"] ?? "");
$newPassword = trim($data["password"] ?? ""); $newPassword = trim($data["password"] ?? "");
@@ -42,7 +42,7 @@ if ($setupMode) {
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user. $isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
} }
// Validate input // Validate input.
if (!$newUsername || !$newPassword) { if (!$newUsername || !$newPassword) {
echo json_encode(["error" => "Username and password required"]); echo json_encode(["error" => "Username and password required"]);
exit; exit;
@@ -54,12 +54,12 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
exit; exit;
} }
// Ensure users.txt exists // Ensure users.txt exists.
if (!file_exists($usersFile)) { if (!file_exists($usersFile)) {
file_put_contents($usersFile, ''); file_put_contents($usersFile, '');
} }
// Check if username already exists // Check if username already exists.
$existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $existingUsers = file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($existingUsers as $line) { foreach ($existingUsers as $line) {
list($storedUser, $storedHash, $storedRole) = explode(':', trim($line)); list($storedUser, $storedHash, $storedRole) = explode(':', trim($line));
@@ -69,10 +69,10 @@ foreach ($existingUsers as $line) {
} }
} }
// Hash the password // Hash the password.
$hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT); $hashedPassword = password_hash($newPassword, PASSWORD_BCRYPT);
// Prepare new user line // Prepare new user line.
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL; $newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
// In setup mode, overwrite users.txt; otherwise, append to it. // In setup mode, overwrite users.txt; otherwise, append to it.

38
auth.js
View File

@@ -37,10 +37,8 @@ function initAuth() {
restoreBtn.classList.add("btn", "btn-warning"); restoreBtn.classList.add("btn", "btn-warning");
// Use a material icon. // Use a material icon.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>'; restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons"); const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) { if (headerButtons) {
// Insert after the third child if available.
if (headerButtons.children.length >= 5) { if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]); headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else { } else {
@@ -54,20 +52,17 @@ function initAuth() {
const removeUserBtn = document.getElementById("removeUserBtn"); const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none"; if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none"; if (removeUserBtn) removeUserBtn.style.display = "none";
// If not admin, hide the restore button.
const restoreBtn = document.getElementById("restoreFilesBtn"); const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) { if (restoreBtn) {
restoreBtn.style.display = "none"; restoreBtn.style.display = "none";
} }
} }
// Set items-per-page.
const selectElem = document.querySelector(".form-control.bottom-select"); const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) { if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10"; const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored; selectElem.value = stored;
} }
} else { } else {
// Do not show a toast message repeatedly during initial check.
toggleVisibility("loginForm", true); toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false); toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false); toggleVisibility("uploadFileForm", false);
@@ -78,7 +73,7 @@ function initAuth() {
console.error("Error checking authentication:", error); console.error("Error checking authentication:", error);
}); });
// Attach login event listener once. // Attach login event listener.
const authForm = document.getElementById("authForm"); const authForm = document.getElementById("authForm");
if (authForm) { if (authForm) {
authForm.addEventListener("submit", function (event) { authForm.addEventListener("submit", function (event) {
@@ -94,7 +89,19 @@ function initAuth() {
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!"); sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
window.location.reload(); window.location.reload();
} else { } else {
showToast("Login failed: " + (data.error || "Unknown error")); if (data.error && data.error.includes("Too many failed login attempts")) {
showToast(data.error);
const loginButton = authForm.querySelector("button[type='submit']");
if (loginButton) {
loginButton.disabled = true;
setTimeout(() => {
loginButton.disabled = false;
showToast("You can now try logging in again.");
}, 30 * 60 * 1000);
}
} else {
showToast("Login failed: " + (data.error || "Unknown error"));
}
} }
}) })
.catch(error => console.error("❌ Error logging in:", error)); .catch(error => console.error("❌ Error logging in:", error));
@@ -119,8 +126,10 @@ function initAuth() {
}); });
document.getElementById("saveUserBtn").addEventListener("click", function () { document.getElementById("saveUserBtn").addEventListener("click", function () {
const newUsername = document.getElementById("newUsername").value.trim(); const newUsername = document.getElementById("newUsername").value.trim();
const newPassword = document.getElementById("newPassword").value.trim(); // Use the new ID for the add user modal's password field.
const newPassword = document.getElementById("addUserPassword").value.trim();
const isAdmin = document.getElementById("isAdmin").checked; const isAdmin = document.getElementById("isAdmin").checked;
console.log("newUsername:", newUsername, "newPassword:", newPassword);
if (!newUsername || !newPassword) { if (!newUsername || !newPassword) {
showToast("Username and password are required!"); showToast("Username and password are required!");
return; return;
@@ -143,7 +152,7 @@ function initAuth() {
if (data.success) { if (data.success) {
showToast("User added successfully!"); showToast("User added successfully!");
closeAddUserModal(); closeAddUserModal();
checkAuthentication(false); // Re-check without showing toast checkAuthentication(false);
} else { } else {
showToast("Error: " + (data.error || "Could not add user")); showToast("Error: " + (data.error || "Could not add user"));
} }
@@ -195,18 +204,18 @@ function initAuth() {
}); });
document.getElementById("changePasswordBtn").addEventListener("click", function() { document.getElementById("changePasswordBtn").addEventListener("click", function() {
// Show the modal. // Show the Change Password modal.
document.getElementById("changePasswordModal").style.display = "block"; document.getElementById("changePasswordModal").style.display = "block";
}); });
document.getElementById("closeChangePasswordModal").addEventListener("click", function() { document.getElementById("closeChangePasswordModal").addEventListener("click", function() {
// Hide the modal. // Hide the Change Password modal.
document.getElementById("changePasswordModal").style.display = "none"; document.getElementById("changePasswordModal").style.display = "none";
}); });
document.getElementById("saveNewPasswordBtn").addEventListener("click", function() { document.getElementById("saveNewPasswordBtn").addEventListener("click", function() {
const oldPassword = document.getElementById("oldPassword").value.trim(); const oldPassword = document.getElementById("oldPassword").value.trim();
const newPassword = document.getElementById("newPassword").value.trim(); const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field
const confirmPassword = document.getElementById("confirmPassword").value.trim(); const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (!oldPassword || !newPassword || !confirmPassword) { if (!oldPassword || !newPassword || !confirmPassword) {
@@ -236,7 +245,7 @@ function initAuth() {
.then(result => { .then(result => {
if (result.success) { if (result.success) {
showToast(result.success); showToast(result.success);
// Optionally clear form fields and close modal. // Clear form fields and close modal.
document.getElementById("oldPassword").value = ""; document.getElementById("oldPassword").value = "";
document.getElementById("newPassword").value = ""; document.getElementById("newPassword").value = "";
document.getElementById("confirmPassword").value = ""; document.getElementById("confirmPassword").value = "";
@@ -253,7 +262,6 @@ function initAuth() {
} }
function checkAuthentication(showLoginToast = true) { function checkAuthentication(showLoginToast = true) {
// Optionally pass a flag so we don't show a toast every time.
return sendRequest("checkAuth.php") return sendRequest("checkAuth.php")
.then(data => { .then(data => {
if (data.setup) { if (data.setup) {
@@ -303,7 +311,7 @@ document.addEventListener("DOMContentLoaded", function () {
function resetUserForm() { function resetUserForm() {
document.getElementById("newUsername").value = ""; document.getElementById("newUsername").value = "";
document.getElementById("newPassword").value = ""; document.getElementById("addUserPassword").value = ""; // Updated for add user modal
} }
function closeAddUserModal() { function closeAddUserModal() {

View File

@@ -4,7 +4,45 @@ header('Content-Type: application/json');
$usersFile = USERS_DIR . USERS_FILE; $usersFile = USERS_DIR . USERS_FILE;
// Function to authenticate user // --- Brute Force Protection Settings ---
$maxAttempts = 5;
$lockoutTime = 30 * 60; // 30 minutes in seconds
$attemptsFile = USERS_DIR . 'failed_logins.json'; // JSON file for tracking failed login attempts
$failedLogFile = USERS_DIR . 'failed_login.log'; // Plain text log for fail2ban
// Load failed attempts data from file.
function loadFailedAttempts($file) {
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
if (is_array($data)) {
return $data;
}
}
return [];
}
// Save failed attempts data to file.
function saveFailedAttempts($file, $data) {
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
}
// Get current IP address.
$ip = $_SERVER['REMOTE_ADDR'];
$currentTime = time();
// Load failed attempts.
$failedAttempts = loadFailedAttempts($attemptsFile);
// Check if this IP is currently locked out.
if (isset($failedAttempts[$ip])) {
$attemptData = $failedAttempts[$ip];
if ($attemptData['count'] >= $maxAttempts && ($currentTime - $attemptData['last_attempt']) < $lockoutTime) {
echo json_encode(["error" => "Too many failed login attempts. Please try again later."]);
exit;
}
}
// --- Authentication Function ---
function authenticate($username, $password) function authenticate($username, $password)
{ {
global $usersFile; global $usersFile;
@@ -23,7 +61,7 @@ function authenticate($username, $password)
return false; return false;
} }
// Get JSON input // Get JSON input.
$data = json_decode(file_get_contents("php://input"), true); $data = json_decode(file_get_contents("php://input"), true);
$username = trim($data["username"] ?? ""); $username = trim($data["username"] ?? "");
$password = trim($data["password"] ?? ""); $password = trim($data["password"] ?? "");
@@ -40,10 +78,15 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
exit; exit;
} }
// Authenticate user // Attempt to authenticate the user.
$userRole = authenticate($username, $password); $userRole = authenticate($username, $password);
if ($userRole !== false) { if ($userRole !== false) {
// Regenerate session ID to mitigate session fixation attacks // On successful login, reset failed attempts for this IP.
if (isset($failedAttempts[$ip])) {
unset($failedAttempts[$ip]);
saveFailedAttempts($attemptsFile, $failedAttempts);
}
// Regenerate session ID to mitigate session fixation attacks.
session_regenerate_id(true); session_regenerate_id(true);
$_SESSION["authenticated"] = true; $_SESSION["authenticated"] = true;
$_SESSION["username"] = $username; $_SESSION["username"] = $username;
@@ -51,5 +94,19 @@ if ($userRole !== false) {
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]); echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
} else { } else {
// On failed login, update failed attempts.
if (isset($failedAttempts[$ip])) {
$failedAttempts[$ip]['count']++;
$failedAttempts[$ip]['last_attempt'] = $currentTime;
} else {
$failedAttempts[$ip] = ['count' => 1, 'last_attempt' => $currentTime];
}
saveFailedAttempts($attemptsFile, $failedAttempts);
// Log the failed attempt to the plain text log for fail2ban.
$logLine = date('Y-m-d H:i:s') . " - Failed login attempt for username: " . $username . " from IP: " . $ip . PHP_EOL;
file_put_contents($failedLogFile, $logLine, FILE_APPEND);
echo json_encode(["error" => "Invalid credentials"]); echo json_encode(["error" => "Invalid credentials"]);
} }
?>

View File

@@ -332,8 +332,8 @@
<h3>Create New User</h3> <h3>Create New User</h3>
<label for="newUsername">Username:</label> <label for="newUsername">Username:</label>
<input type="text" id="newUsername" class="form-control" /> <input type="text" id="newUsername" class="form-control" />
<label for="newPassword">Password:</label> <label for="addUserPassword">Password:</label>
<input type="password" id="newPassword" class="form-control" /> <input type="password" id="addUserPassword" class="form-control" />
<div id="adminCheckboxContainer"> <div id="adminCheckboxContainer">
<input type="checkbox" id="isAdmin" /> <input type="checkbox" id="isAdmin" />
<label for="isAdmin">Grant Admin Access</label> <label for="isAdmin">Grant Admin Access</label>