Harden security: enable CSP, add SRI, and externalize inline scripts
This commit is contained in:
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 4/26/2025
|
||||||
|
|
||||||
|
### 1. Apache / Dockerfile (CSP)
|
||||||
|
|
||||||
|
- Enabled Apache’s `mod_headers` in the Dockerfile (`a2enmod headers ssl deflate expires proxy proxy_fcgi rewrite`)
|
||||||
|
- Added a strong `Content-Security-Policy` header in the vhost configs to lock down allowed sources for scripts, styles, fonts, images, and connections
|
||||||
|
|
||||||
|
### 2. index.html & CDN Includes
|
||||||
|
|
||||||
|
- Applied Subresource Integrity (`integrity` + `crossorigin="anonymous"`) to all static CDN assets (Bootstrap CSS, CodeMirror CSS/JS, Resumable.js, DOMPurify, Fuse.js)
|
||||||
|
- Omitted SRI on Google Fonts & Material Icons links (dynamic per-browser CSS)
|
||||||
|
- Removed all inline `<script>` and `onclick` attributes; now all behaviors live in external JS modules
|
||||||
|
|
||||||
|
### 3. auth.js (Logout Handling)
|
||||||
|
|
||||||
|
- Moved the logout-on-`?logout=1` snippet from inline HTML into `auth.js`
|
||||||
|
- In `DOMContentLoaded`, attached a `click` listener to `#logoutBtn` that POSTs to `/api/auth/logout.php` and reloads
|
||||||
|
|
||||||
|
### 4. fileActions.js (Modal Button Handlers)
|
||||||
|
|
||||||
|
- Externalized the cancel/download buttons for single-file and ZIP-download modals by adding `click` listeners in `fileActions.js`
|
||||||
|
- Removed the inline `onclick` attributes from `#cancelDownloadFile` and `#confirmSingleDownloadButton` in the HTML
|
||||||
|
- Ensured all file-action modals (delete, download, extract, copy, move, rename) now use JS event handlers instead of inline code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 4/25/2025
|
## Changes 4/25/2025
|
||||||
|
|
||||||
- Switch single‐file download to native `<a>` link (no JS buffering)
|
- Switch single‐file download to native `<a>` link (no JS buffering)
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ RUN cat <<'EOF' > /etc/apache2/sites-available/000-default.conf
|
|||||||
Header always set X-Content-Type-Options "nosniff"
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
Header always set X-XSS-Protection "1; mode=block"
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
# Compression
|
# Compression
|
||||||
|
|||||||
@@ -5,13 +5,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title data-i18n-key="title">FileRise</title>
|
<title data-i18n-key="title">FileRise</title>
|
||||||
<script>
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
if (params.get('logout') === '1') {
|
|
||||||
localStorage.removeItem("username");
|
|
||||||
localStorage.removeItem("userTOTPEnabled");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||||
<meta name="csrf-token" content="">
|
<meta name="csrf-token" content="">
|
||||||
@@ -20,9 +13,12 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
|
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"
|
||||||
|
integrity="sha384-zaeBlB/vwYsDRSlFajnDd7OydJ0cWk+c2OWybl3eSUf6hW2EbhlCsQPqKr3gkznT" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css"
|
||||||
|
integrity="sha384-eZTPTN0EvJdn23s24UDYJmUM2T7C2ZFa3qFLypeBruJv8mZeTusKUAO/j5zPAQ6l" crossorigin="anonymous">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"
|
||||||
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
@@ -396,27 +392,23 @@
|
|||||||
</div> <!-- end mainColumn -->
|
</div> <!-- end mainColumn -->
|
||||||
</div> <!-- end main-wrapper -->
|
</div> <!-- end main-wrapper -->
|
||||||
|
|
||||||
<!-- Download Progress Modal -->
|
<!-- Download Progress Modal -->
|
||||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||||
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||||
Preparing your download...
|
Preparing your download...
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<!-- spinner -->
|
<!-- spinner -->
|
||||||
<span class="material-icons download-spinner">autorenew</span>
|
<span class="material-icons download-spinner">autorenew</span>
|
||||||
|
|
||||||
<!-- these were missing -->
|
<!-- these were missing -->
|
||||||
<progress
|
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
|
||||||
id="downloadProgressBar"
|
<p>
|
||||||
value="0" max="100"
|
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||||
style="width:100%; height:1.5em; display:none;"
|
</p>
|
||||||
></progress>
|
</div>
|
||||||
<p>
|
|
||||||
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Single File Download Modal -->
|
<!-- Single File Download Modal -->
|
||||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||||
@@ -426,11 +418,8 @@
|
|||||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
|
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
|
||||||
placeholder="Filename" />
|
placeholder="Filename" />
|
||||||
<div style="margin-top: 15px; text-align: right;">
|
<div style="margin-top: 15px; text-align: right;">
|
||||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
<button id="cancelDownloadFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
<button id="confirmSingleDownloadButton" class="btn btn-primary" data-i18n-key="download">Download</button>
|
||||||
data-i18n-key="cancel">Cancel</button>
|
|
||||||
<button id="confirmSingleDownloadButton" class="btn btn-primary" onclick="confirmSingleDownload()"
|
|
||||||
data-i18n-key="download">Download</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -437,12 +437,26 @@ function initAuth() {
|
|||||||
submitLogin(formData);
|
submitLogin(formData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
// handle ?logout=1 query
|
||||||
fetch("/api/auth/logout.php", {
|
const params = new URLSearchParams(window.location.search);
|
||||||
method: "POST",
|
if (params.get('logout') === '1') {
|
||||||
credentials: "include",
|
localStorage.removeItem("username");
|
||||||
headers: { "X-CSRF-Token": window.csrfToken }
|
localStorage.removeItem("userTOTPEnabled");
|
||||||
}).then(() => window.location.reload(true)).catch(() => { });
|
}
|
||||||
|
|
||||||
|
// attach logout button listener
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const btn = document.getElementById('logoutBtn');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
fetch('/api/auth/logout.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'X-CSRF-Token': window.csrfToken }
|
||||||
|
})
|
||||||
|
.then(() => window.location.reload(true))
|
||||||
|
.catch(() => { });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
|
|||||||
@@ -193,10 +193,10 @@ export function handleExtractZipSelected(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const zipNameModal = document.getElementById("downloadZipModal");
|
const zipNameModal = document.getElementById("downloadZipModal");
|
||||||
const progressModal = document.getElementById("downloadProgressModal");
|
const progressModal = document.getElementById("downloadProgressModal");
|
||||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||||
|
|
||||||
// 1) Cancel button hides the name modal
|
// 1) Cancel button hides the name modal
|
||||||
if (cancelZipBtn) {
|
if (cancelZipBtn) {
|
||||||
@@ -219,8 +219,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// b) Hide the name‐input modal, show the spinner modal
|
// b) Hide the name‐input modal, show the spinner modal
|
||||||
zipNameModal.style.display = "none";
|
zipNameModal.style.display = "none";
|
||||||
progressModal.style.display = "block";
|
progressModal.style.display = "block";
|
||||||
|
|
||||||
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
||||||
const titleEl = document.getElementById("downloadProgressTitle");
|
const titleEl = document.getElementById("downloadProgressTitle");
|
||||||
@@ -233,11 +233,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-CSRF-Token": window.csrfToken
|
"X-CSRF-Token": window.csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
folder: window.currentFolder || "root",
|
folder: window.currentFolder || "root",
|
||||||
files: window.filesToDownload
|
files: window.filesToDownload
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -252,8 +252,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
// e) Hand off to the browser’s download manager
|
// e) Hand off to the browser’s download manager
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = zipName;
|
a.download = zipName;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
@@ -555,4 +555,22 @@ export function initFileActions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook up the single‐file download modal buttons
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const cancelDownloadFileBtn = document.getElementById("cancelDownloadFile");
|
||||||
|
if (cancelDownloadFileBtn) {
|
||||||
|
cancelDownloadFileBtn.addEventListener("click", () => {
|
||||||
|
document.getElementById("downloadFileModal").style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmSingleDownloadBtn = document.getElementById("confirmSingleDownloadButton");
|
||||||
|
if (confirmSingleDownloadBtn) {
|
||||||
|
confirmSingleDownloadBtn.addEventListener("click", confirmSingleDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make Enter also confirm the download
|
||||||
|
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
|
||||||
|
});
|
||||||
|
|
||||||
window.renameFile = renameFile;
|
window.renameFile = renameFile;
|
||||||
Reference in New Issue
Block a user