Add CSRF protections to state-changing endpoints
This commit is contained in:
@@ -27,6 +27,7 @@ Multi File Upload Editor is a lightweight, secure web application for uploading,
|
||||
- The file list can be sorted by name, last modified date, upload date, size, or uploader. For easier browsing, the interface supports pagination with selectable page sizes (10, 20, 50, or 100 items per page) and navigation controls (“Prev”, “Next”, specific page numbers).
|
||||
- **User Authentication & Management:**
|
||||
- Secure, session-based authentication protects the editor. An admin user can add or remove users through the interface. Passwords are hashed using PHP’s password_hash() for security, and session checks prevent unauthorized access to backend endpoints.
|
||||
- **CSRF Protection:** All state-changing endpoints (such as those for folder and file operations) include CSRF token validation to ensure that only legitimate requests from authenticated users are processed.
|
||||
- **Responsive, Dynamic & Persistent UI:**
|
||||
- The interface is mobile-friendly and adjusts to different screen sizes (hiding non-critical columns on small devices to avoid clutter). Updates to the file list, folder tree, and upload progress happen asynchronously (via Fetch API and XMLHttpRequest), so the page never needs to fully reload. Users receive immediate feedback through toast notifications and modal dialogs for actions like confirmations and error messages, creating a smooth user experience. Persistent UI elements Items Per Page, Dark/Light Mode, folder tree view & last open folder.
|
||||
- **Dark Mode/Light Mode**
|
||||
|
||||
@@ -3,6 +3,13 @@ require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Determine if we are in setup mode:
|
||||
// - Query parameter setup=1 is passed
|
||||
|
||||
24
auth.js
24
auth.js
@@ -16,8 +16,8 @@ function initAuth() {
|
||||
password: document.getElementById("loginPassword").value.trim()
|
||||
};
|
||||
console.log("Sending login data:", formData);
|
||||
// sendRequest already handles credentials if configured in networkUtils.js.
|
||||
sendRequest("auth.php", "POST", formData)
|
||||
// Include CSRF token header with login
|
||||
sendRequest("auth.php", "POST", formData, { "X-CSRF-Token": window.csrfToken })
|
||||
.then(data => {
|
||||
console.log("Login response:", data);
|
||||
if (data.success) {
|
||||
@@ -35,7 +35,8 @@ function initAuth() {
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include" // Ensure the session cookie is sent.
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
})
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(error => console.error("Logout error:", error));
|
||||
@@ -62,7 +63,10 @@ function initAuth() {
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -101,7 +105,10 @@ function initAuth() {
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -129,7 +136,6 @@ function checkAuthentication() {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
showToast("Setup mode: No users found. Please add an admin user.");
|
||||
// In setup mode, hide login and main operations; show Add User modal.
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
@@ -143,7 +149,6 @@ function checkAuthentication() {
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
// Show Add/Remove User buttons if admin.
|
||||
if (data.isAdmin) {
|
||||
const addUserBtn = document.getElementById("addUserBtn");
|
||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||
@@ -156,7 +161,6 @@ function checkAuthentication() {
|
||||
if (removeUserBtn) removeUserBtn.style.display = "none";
|
||||
}
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
// Update persistent items-per-page select once main operations are visible.
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||
@@ -183,18 +187,15 @@ window.checkAuthentication = checkAuthentication;
|
||||
/* ------------------------------
|
||||
Persistent Items-Per-Page Setting
|
||||
------------------------------ */
|
||||
// When the select value changes, save it to localStorage and refresh the file list.
|
||||
window.changeItemsPerPage = function (value) {
|
||||
console.log("Saving itemsPerPage:", value);
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
// Refresh the file list automatically.
|
||||
const folder = window.currentFolder || "root";
|
||||
if (typeof renderFileTable === "function") {
|
||||
renderFileTable(folder);
|
||||
}
|
||||
};
|
||||
|
||||
// On DOMContentLoaded, set the select to the persisted value.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
@@ -207,7 +208,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
/* ------------------------------
|
||||
Helper functions for modals and user list
|
||||
------------------------------ */
|
||||
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
|
||||
7
auth.php
7
auth.php
@@ -3,6 +3,13 @@ require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
/*$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}*/
|
||||
|
||||
// Function to authenticate user
|
||||
function authenticate($username, $password) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
session_set_cookie_params(7200); // 2 hours in seconds
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
// config.php
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('BASE_URL', 'http://yourwebsite/uploads/');
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
|
||||
@@ -15,6 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folderName'])) {
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
|
||||
@@ -15,6 +15,15 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['folder'])) {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// fileManager.js
|
||||
|
||||
import {
|
||||
escapeHTML,
|
||||
debounce,
|
||||
@@ -21,11 +19,8 @@ window.itemsPerPage = window.itemsPerPage || 10;
|
||||
window.currentPage = window.currentPage || 1;
|
||||
|
||||
// --- Define formatFolderName ---
|
||||
// This helper formats folder names for display. Adjust as needed.
|
||||
function formatFolderName(folder) {
|
||||
// Example: If folder is "root", return "(Root)"
|
||||
if (folder === "root") return "(Root)";
|
||||
// Replace underscores/dashes with spaces and capitalize each word.
|
||||
return folder
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
@@ -247,7 +242,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
confirmDelete.addEventListener("click", function () {
|
||||
fetch("deleteFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -303,7 +302,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
fetch("downloadZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: folder, files: window.filesToDownload })
|
||||
})
|
||||
.then(response => {
|
||||
@@ -396,7 +398,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
fetch("copyFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ source: window.currentFolder, files: window.filesToCopy, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -452,7 +458,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
fetch("moveFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ source: window.currentFolder, files: window.filesToMove, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -632,7 +642,11 @@ export function saveFile(fileName, folder) {
|
||||
};
|
||||
fetch("saveFile.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify(fileDataObj)
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -708,7 +722,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const folderUsed = window.fileFolder;
|
||||
fetch("renameFile.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -267,7 +267,9 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function
|
||||
document.getElementById("newRenameFolderName").value = "";
|
||||
});
|
||||
|
||||
document.getElementById("submitRenameFolder").addEventListener("click", function () {
|
||||
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const newNameBasename = document.getElementById("newRenameFolderName").value.trim();
|
||||
if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) {
|
||||
@@ -276,9 +278,22 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
|
||||
}
|
||||
const parentPath = getParentFolder(selectedFolder);
|
||||
const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename;
|
||||
|
||||
// Read the CSRF token from the meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
if (!csrfToken) {
|
||||
showToast("CSRF token not loaded yet! Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the rename request with the CSRF token in a custom header
|
||||
fetch("renameFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include", // ensure cookies (and session) are sent
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderFull })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -316,9 +331,15 @@ document.getElementById("cancelDeleteFolder").addEventListener("click", function
|
||||
|
||||
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
// Read CSRF token from the meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
fetch("deleteFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({ folder: selectedFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -358,10 +379,19 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
||||
if (selectedFolder && selectedFolder !== "root") {
|
||||
fullFolderName = selectedFolder + "/" + folderInput;
|
||||
}
|
||||
// Read CSRF token from the meta tag
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
fetch("createFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderName: folderInput, parent: selectedFolder === "root" ? "" : selectedFolder })
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folderName: folderInput,
|
||||
parent: selectedFolder === "root" ? "" : selectedFolder
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Multi File Upload Editor</title>
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="" />
|
||||
<!-- External CSS -->
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<!-- Google Fonts and Material Icons -->
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?php
|
||||
session_start();
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$_SESSION = []; // Clear session data
|
||||
session_destroy(); // Destroy session
|
||||
|
||||
|
||||
19
main.js
19
main.js
@@ -17,6 +17,25 @@ import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
|
||||
function loadCsrfToken() {
|
||||
fetch('token.php', { credentials: 'include' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Assign to global variable
|
||||
window.csrfToken = data.csrf_token;
|
||||
// Also update the meta tag
|
||||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.name = 'csrf-token';
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', data.csrf_token);
|
||||
})
|
||||
.catch(error => console.error("Error loading CSRF token:", error));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadCsrfToken);
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
|
||||
@@ -3,6 +3,13 @@ require 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$usersFile = USERS_DIR . USERS_FILE;
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Only allow admins to remove users
|
||||
if (
|
||||
|
||||
@@ -5,6 +5,16 @@ header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
|
||||
@@ -18,6 +18,16 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF Protection: Read token from the custom header "X-CSRF-Token"
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid CSRF token.']);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the JSON input and decode it
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!isset($input['oldFolder']) || !isset($input['newFolder'])) {
|
||||
@@ -28,22 +38,19 @@ if (!isset($input['oldFolder']) || !isset($input['newFolder'])) {
|
||||
$oldFolder = trim($input['oldFolder']);
|
||||
$newFolder = trim($input['newFolder']);
|
||||
|
||||
// Allow letters, numbers, underscores, dashes, spaces, and forward slashes
|
||||
// Validate folder names
|
||||
if (!preg_match('/^[A-Za-z0-9_\- \/]+$/', $oldFolder) || !preg_match('/^[A-Za-z0-9_\- \/]+$/', $newFolder)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid folder name(s).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Trim any leading/trailing slashes and spaces.
|
||||
$oldFolder = trim($oldFolder, "/\\ ");
|
||||
$newFolder = trim($newFolder, "/\\ ");
|
||||
|
||||
// Build full paths relative to UPLOAD_DIR.
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder;
|
||||
$newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder;
|
||||
|
||||
// Security check: ensure both paths are within the base directory.
|
||||
if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||
strpos(realpath($oldPath), realpath($baseDir)) !== 0 ||
|
||||
strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0) {
|
||||
@@ -51,13 +58,11 @@ if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) ||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the folder to rename exists.
|
||||
if (!file_exists($oldPath) || !is_dir($oldPath)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Folder to rename does not exist.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the new folder name already exists.
|
||||
if (file_exists($newPath)) {
|
||||
echo json_encode(['success' => false, 'error' => 'New folder name already exists.']);
|
||||
exit;
|
||||
|
||||
10
saveFile.php
10
saveFile.php
@@ -2,6 +2,16 @@
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
|
||||
5
token.php
Normal file
5
token.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require 'config.php'; // Must call session_start() and generate CSRF token if not set
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["csrf_token" => $_SESSION['csrf_token']]);
|
||||
?>
|
||||
37
upload.js
37
upload.js
@@ -7,8 +7,6 @@ function traverseFileTreePromise(item, path = "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (item.isFile) {
|
||||
item.file(file => {
|
||||
// Instead of modifying file.webkitRelativePath (read-only),
|
||||
// define a new property called "customRelativePath"
|
||||
Object.defineProperty(file, 'customRelativePath', {
|
||||
value: path + file.name,
|
||||
writable: true,
|
||||
@@ -23,9 +21,7 @@ function traverseFileTreePromise(item, path = "") {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
promises.push(traverseFileTreePromise(entries[i], path + item.name + "/"));
|
||||
}
|
||||
Promise.all(promises).then(results => {
|
||||
resolve(results.flat());
|
||||
});
|
||||
Promise.all(promises).then(results => resolve(results.flat()));
|
||||
});
|
||||
} else {
|
||||
resolve([]);
|
||||
@@ -46,7 +42,6 @@ function getFilesFromDataTransferItems(items) {
|
||||
}
|
||||
|
||||
// Helper: Set default drop area content.
|
||||
// Moved to module scope so it is available globally in this module.
|
||||
function setDropAreaDefault() {
|
||||
const dropArea = document.getElementById("uploadDropArea");
|
||||
if (dropArea) {
|
||||
@@ -104,7 +99,6 @@ function updateFileInfoCount() {
|
||||
<span id="fileCountDisplay" class="file-name-display">${window.selectedFiles.length} files selected</span>
|
||||
`;
|
||||
}
|
||||
// Show preview of first file.
|
||||
const previewContainer = document.getElementById("filePreviewContainer");
|
||||
if (previewContainer && window.selectedFiles.length > 0) {
|
||||
previewContainer.innerHTML = "";
|
||||
@@ -120,19 +114,16 @@ function createFileEntry(file) {
|
||||
li.style.display = "flex";
|
||||
li.dataset.uploadIndex = file.uploadIndex;
|
||||
|
||||
// Create remove button positioned to the left of the preview.
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.classList.add("remove-file-btn");
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
// Remove file from global selected files array.
|
||||
const uploadIndex = file.uploadIndex;
|
||||
window.selectedFiles = window.selectedFiles.filter(f => f.uploadIndex !== uploadIndex);
|
||||
li.remove();
|
||||
updateFileInfoCount();
|
||||
});
|
||||
// Store the button so we can hide it later when upload completes.
|
||||
li.removeBtn = removeBtn;
|
||||
|
||||
const preview = document.createElement("div");
|
||||
@@ -154,8 +145,6 @@ function createFileEntry(file) {
|
||||
progBar.innerText = "0%";
|
||||
|
||||
progDiv.appendChild(progBar);
|
||||
|
||||
// Append in order: remove button, preview, name, progress.
|
||||
li.appendChild(removeBtn);
|
||||
li.appendChild(preview);
|
||||
li.appendChild(nameDiv);
|
||||
@@ -171,7 +160,6 @@ function processFiles(filesInput) {
|
||||
const fileInfoContainer = document.getElementById("fileInfoContainer");
|
||||
const files = Array.from(filesInput);
|
||||
|
||||
// Update file info container with preview and file count.
|
||||
if (fileInfoContainer) {
|
||||
if (files.length > 0) {
|
||||
if (files.length === 1) {
|
||||
@@ -195,12 +183,10 @@ function processFiles(filesInput) {
|
||||
}
|
||||
}
|
||||
|
||||
// Assign unique uploadIndex to each file.
|
||||
files.forEach((file, index) => {
|
||||
file.uploadIndex = index;
|
||||
});
|
||||
|
||||
// Build progress list.
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
progressContainer.innerHTML = "";
|
||||
|
||||
@@ -209,14 +195,12 @@ function processFiles(filesInput) {
|
||||
const list = document.createElement("ul");
|
||||
list.classList.add("upload-progress-list");
|
||||
|
||||
// Determine grouping using relative path.
|
||||
const hasRelativePaths = files.some(file => {
|
||||
const rel = file.webkitRelativePath || file.customRelativePath || "";
|
||||
return rel.trim() !== "";
|
||||
});
|
||||
|
||||
if (hasRelativePaths) {
|
||||
// Group files by folder.
|
||||
const fileGroups = {};
|
||||
files.forEach(file => {
|
||||
let folderName = "Root";
|
||||
@@ -233,15 +217,12 @@ function processFiles(filesInput) {
|
||||
fileGroups[folderName].push(file);
|
||||
});
|
||||
|
||||
// Create list elements for each folder group.
|
||||
Object.keys(fileGroups).forEach(folderName => {
|
||||
// Folder header with Material Icon.
|
||||
const folderLi = document.createElement("li");
|
||||
folderLi.classList.add("upload-folder-group");
|
||||
folderLi.innerHTML = `<i class="material-icons folder-icon" style="vertical-align:middle; margin-right:8px;">folder</i> ${folderName}:`;
|
||||
list.appendChild(folderLi);
|
||||
|
||||
// Nested list for files.
|
||||
const nestedUl = document.createElement("ul");
|
||||
nestedUl.classList.add("upload-folder-group-list");
|
||||
fileGroups[folderName]
|
||||
@@ -253,7 +234,6 @@ function processFiles(filesInput) {
|
||||
list.appendChild(nestedUl);
|
||||
});
|
||||
} else {
|
||||
// Flat list.
|
||||
files.forEach((file, index) => {
|
||||
const li = createFileEntry(file);
|
||||
li.style.display = (index < maxDisplay) ? "flex" : "none";
|
||||
@@ -270,19 +250,15 @@ function processFiles(filesInput) {
|
||||
}
|
||||
const listWrapper = document.createElement("div");
|
||||
listWrapper.classList.add("upload-progress-wrapper");
|
||||
// Set a maximum height and enable vertical scrolling.
|
||||
listWrapper.style.maxHeight = "300px";
|
||||
listWrapper.style.overflowY = "auto";
|
||||
listWrapper.appendChild(list);
|
||||
progressContainer.appendChild(listWrapper);
|
||||
}
|
||||
|
||||
// Call once on page load:
|
||||
adjustFolderHelpExpansion();
|
||||
// Also call on window resize:
|
||||
window.addEventListener("resize", adjustFolderHelpExpansion);
|
||||
|
||||
// Store files globally for submission.
|
||||
window.selectedFiles = files;
|
||||
updateFileInfoCount();
|
||||
}
|
||||
@@ -293,7 +269,6 @@ function submitFiles(allFiles) {
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
const fileInput = document.getElementById("file");
|
||||
|
||||
// Map uploadIndex to progress element.
|
||||
const progressElements = {};
|
||||
const listItems = progressContainer.querySelectorAll("li.upload-progress-item");
|
||||
listItems.forEach(item => {
|
||||
@@ -308,6 +283,8 @@ function submitFiles(allFiles) {
|
||||
const formData = new FormData();
|
||||
formData.append("file[]", file);
|
||||
formData.append("folder", folderToUse);
|
||||
// Append CSRF token as "upload_token"
|
||||
formData.append("upload_token", window.csrfToken);
|
||||
const relativePath = file.webkitRelativePath || file.customRelativePath || "";
|
||||
if (relativePath.trim() !== "") {
|
||||
formData.append("relativePath", relativePath);
|
||||
@@ -346,7 +323,6 @@ function submitFiles(allFiles) {
|
||||
if (li) {
|
||||
li.progressBar.style.width = "100%";
|
||||
li.progressBar.innerText = "Done";
|
||||
// Hide the remove button now that upload is done.
|
||||
if (li.removeBtn) {
|
||||
li.removeBtn.style.display = "none";
|
||||
}
|
||||
@@ -391,6 +367,8 @@ function submitFiles(allFiles) {
|
||||
});
|
||||
|
||||
xhr.open("POST", "upload.php", true);
|
||||
// Set the CSRF token header to match the folderManager approach.
|
||||
xhr.setRequestHeader("X-CSRF-Token", window.csrfToken);
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
@@ -400,7 +378,6 @@ function submitFiles(allFiles) {
|
||||
initFileActions();
|
||||
serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase());
|
||||
allFiles.forEach(file => {
|
||||
// Skip verification for folder-uploaded files.
|
||||
if ((file.webkitRelativePath || file.customRelativePath || "").trim() !== "") {
|
||||
return;
|
||||
}
|
||||
@@ -415,7 +392,6 @@ function submitFiles(allFiles) {
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (fileInput) fileInput.value = "";
|
||||
// Hide remove buttons in progress container.
|
||||
const removeBtns = progressContainer.querySelectorAll("button.remove-file-btn");
|
||||
removeBtns.forEach(btn => btn.style.display = "none");
|
||||
progressContainer.innerHTML = "";
|
||||
@@ -450,15 +426,12 @@ function initUpload() {
|
||||
const uploadForm = document.getElementById("uploadFileForm");
|
||||
|
||||
if (fileInput) {
|
||||
// Remove folder selection attributes so clicking the input shows files:
|
||||
fileInput.removeAttribute("webkitdirectory");
|
||||
fileInput.removeAttribute("mozdirectory");
|
||||
fileInput.removeAttribute("directory");
|
||||
// Allow selecting multiple files.
|
||||
fileInput.setAttribute("multiple", "");
|
||||
}
|
||||
|
||||
// Set default drop area content.
|
||||
setDropAreaDefault();
|
||||
|
||||
if (dropArea) {
|
||||
|
||||
25
upload.php
25
upload.php
@@ -2,6 +2,17 @@
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection for Uploads ---
|
||||
// Use getallheaders() to read the token from the header.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
@@ -9,9 +20,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate folder name input. Allow letters, numbers, underscores, dashes, spaces, and forward slashes.
|
||||
// Validate folder name input.
|
||||
$folder = isset($_POST['folder']) ? trim($_POST['folder']) : 'root';
|
||||
// When folder is not 'root', allow "/" in the folder name to denote subfolders.
|
||||
if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) {
|
||||
echo json_encode(["error" => "Invalid folder name"]);
|
||||
exit;
|
||||
@@ -22,7 +32,6 @@ $uploadDir = UPLOAD_DIR;
|
||||
if ($folder !== 'root') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($uploadDir)) {
|
||||
// Recursively create subfolders as needed.
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
} else {
|
||||
@@ -36,24 +45,18 @@ $metadataFile = META_DIR . META_FILE;
|
||||
$metadata = file_exists($metadataFile) ? json_decode(file_get_contents($metadataFile), true) : [];
|
||||
$metadataChanged = false;
|
||||
|
||||
// Define a safe pattern for file names: letters, numbers, underscores, dashes, dots, and spaces.
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/';
|
||||
|
||||
foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
// Use basename to strip any directory components.
|
||||
$safeFileName = basename($fileName);
|
||||
|
||||
// Validate that the sanitized file name contains only allowed characters.
|
||||
if (!preg_match($safeFileNamePattern, $safeFileName)) {
|
||||
echo json_encode(["error" => "Invalid file name: " . $fileName]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Minimal Folder/Subfolder Logic ---
|
||||
// Check if a relativePath was provided (from a folder upload)
|
||||
$relativePath = '';
|
||||
if (isset($_POST['relativePath'])) {
|
||||
// In case of multiple files, relativePath may be an array.
|
||||
if (is_array($_POST['relativePath'])) {
|
||||
$relativePath = $_POST['relativePath'][$index] ?? '';
|
||||
} else {
|
||||
@@ -61,10 +64,8 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
}
|
||||
}
|
||||
if (!empty($relativePath)) {
|
||||
// Extract the directory part from the relative path.
|
||||
$subDir = dirname($relativePath);
|
||||
if ($subDir !== '.' && $subDir !== '') {
|
||||
// If uploading to root, don't add the "root" folder in the path.
|
||||
if ($folder === 'root') {
|
||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
@@ -73,7 +74,6 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
// Use the basename from the relative path.
|
||||
$safeFileName = basename($relativePath);
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,6 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) {
|
||||
$targetPath = $uploadDir . $safeFileName;
|
||||
|
||||
if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) {
|
||||
// Build the metadata key.
|
||||
if (!empty($relativePath)) {
|
||||
$metaKey = ($folder !== 'root') ? $folder . "/" . $relativePath : $relativePath;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user