added brute force protection
This commit is contained in:
16
addUser.php
16
addUser.php
@@ -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
38
auth.js
@@ -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() {
|
||||
|
||||
65
auth.php
65
auth.php
@@ -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"]);
|
||||
}
|
||||
?>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user