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:
// - Query parameter setup=1 is passed
// - 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)) === '')) {
// Allow initial admin creation without session checks.
$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.
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$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"]);
http_response_code(403);
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);
$newUsername = trim($data["username"] ?? "");
$newPassword = trim($data["password"] ?? "");
@@ -42,7 +42,7 @@ if ($setupMode) {
$isAdmin = !empty($data["isAdmin"]) ? "1" : "0"; // "1" for admin, "0" for regular user.
}
// Validate input
// Validate input.
if (!$newUsername || !$newPassword) {
echo json_encode(["error" => "Username and password required"]);
exit;
@@ -54,12 +54,12 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $newUsername)) {
exit;
}
// Ensure users.txt exists
// Ensure users.txt exists.
if (!file_exists($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);
foreach ($existingUsers as $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);
// Prepare new user line
// Prepare new user line.
$newUserLine = $newUsername . ":" . $hashedPassword . ":" . $isAdmin . PHP_EOL;
// 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");
// Use a material icon.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
// Insert after the third child if available.
if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else {
@@ -54,20 +52,17 @@ function initAuth() {
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
// If not admin, hide the restore button.
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) {
restoreBtn.style.display = "none";
}
}
// Set items-per-page.
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
} else {
// Do not show a toast message repeatedly during initial check.
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
@@ -78,7 +73,7 @@ function initAuth() {
console.error("Error checking authentication:", error);
});
// Attach login event listener once.
// Attach login event listener.
const authForm = document.getElementById("authForm");
if (authForm) {
authForm.addEventListener("submit", function (event) {
@@ -94,7 +89,19 @@ function initAuth() {
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
window.location.reload();
} 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));
@@ -119,8 +126,10 @@ function initAuth() {
});
document.getElementById("saveUserBtn").addEventListener("click", function () {
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;
console.log("newUsername:", newUsername, "newPassword:", newPassword);
if (!newUsername || !newPassword) {
showToast("Username and password are required!");
return;
@@ -143,7 +152,7 @@ function initAuth() {
if (data.success) {
showToast("User added successfully!");
closeAddUserModal();
checkAuthentication(false); // Re-check without showing toast
checkAuthentication(false);
} else {
showToast("Error: " + (data.error || "Could not add user"));
}
@@ -195,18 +204,18 @@ function initAuth() {
});
document.getElementById("changePasswordBtn").addEventListener("click", function() {
// Show the modal.
// Show the Change Password modal.
document.getElementById("changePasswordModal").style.display = "block";
});
document.getElementById("closeChangePasswordModal").addEventListener("click", function() {
// Hide the modal.
// Hide the Change Password modal.
document.getElementById("changePasswordModal").style.display = "none";
});
document.getElementById("saveNewPasswordBtn").addEventListener("click", function() {
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();
if (!oldPassword || !newPassword || !confirmPassword) {
@@ -236,7 +245,7 @@ function initAuth() {
.then(result => {
if (result.success) {
showToast(result.success);
// Optionally clear form fields and close modal.
// Clear form fields and close modal.
document.getElementById("oldPassword").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("confirmPassword").value = "";
@@ -253,7 +262,6 @@ function initAuth() {
}
function checkAuthentication(showLoginToast = true) {
// Optionally pass a flag so we don't show a toast every time.
return sendRequest("checkAuth.php")
.then(data => {
if (data.setup) {
@@ -303,7 +311,7 @@ document.addEventListener("DOMContentLoaded", function () {
function resetUserForm() {
document.getElementById("newUsername").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("addUserPassword").value = ""; // Updated for add user modal
}
function closeAddUserModal() {

View File

@@ -4,7 +4,45 @@ header('Content-Type: application/json');
$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)
{
global $usersFile;
@@ -23,7 +61,7 @@ function authenticate($username, $password)
return false;
}
// Get JSON input
// Get JSON input.
$data = json_decode(file_get_contents("php://input"), true);
$username = trim($data["username"] ?? "");
$password = trim($data["password"] ?? "");
@@ -40,10 +78,15 @@ if (!preg_match('/^[A-Za-z0-9_\- ]+$/', $username)) {
exit;
}
// Authenticate user
// Attempt to authenticate the user.
$userRole = authenticate($username, $password);
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["authenticated"] = true;
$_SESSION["username"] = $username;
@@ -51,5 +94,19 @@ if ($userRole !== false) {
echo json_encode(["success" => "Login successful", "isAdmin" => $_SESSION["isAdmin"]]);
} 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"]);
}
?>

View File

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