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:
|
// 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.
|
||||||
|
|||||||
36
auth.js
36
auth.js
@@ -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) {
|
||||||
@@ -93,9 +88,21 @@ function initAuth() {
|
|||||||
console.log("✅ Login successful. Reloading page.");
|
console.log("✅ Login successful. Reloading page.");
|
||||||
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
sessionStorage.setItem("welcomeMessage", "Welcome back, " + formData.username + "!");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
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 {
|
} else {
|
||||||
showToast("Login failed: " + (data.error || "Unknown error"));
|
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() {
|
||||||
|
|||||||
65
auth.php
65
auth.php
@@ -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"]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user