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
|
||||
|
||||
## 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
|
||||
|
||||
- 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-XSS-Protection "1; mode=block"
|
||||
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>
|
||||
|
||||
# Compression
|
||||
|
||||
@@ -5,13 +5,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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/svg+xml" href="/assets/logo.svg">
|
||||
<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/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/material-darker.min.css">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||
<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"
|
||||
integrity="sha384-UXbkZAbZYZ/KCAslc6UO4d6UHNKsOxZ/sqROSQaPTZCuEIKhfbhmffQ64uXFOcma"
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -396,27 +392,23 @@
|
||||
</div> <!-- end mainColumn -->
|
||||
</div> <!-- end main-wrapper -->
|
||||
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||
Preparing your download...
|
||||
</h4>
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
<h4 id="downloadProgressTitle" data-i18n-key="preparing_download">
|
||||
Preparing your download...
|
||||
</h4>
|
||||
|
||||
<!-- spinner -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
<!-- spinner -->
|
||||
<span class="material-icons download-spinner">autorenew</span>
|
||||
|
||||
<!-- these were missing -->
|
||||
<progress
|
||||
id="downloadProgressBar"
|
||||
value="0" max="100"
|
||||
style="width:100%; height:1.5em; display:none;"
|
||||
></progress>
|
||||
<p>
|
||||
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||
</p>
|
||||
<!-- these were missing -->
|
||||
<progress id="downloadProgressBar" value="0" max="100" style="width:100%; height:1.5em; display:none;"></progress>
|
||||
<p>
|
||||
<span id="downloadProgressPercent" style="display:none;">0%</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Single File Download Modal -->
|
||||
<div id="downloadFileModal" class="modal" style="display: none;">
|
||||
@@ -426,11 +418,8 @@
|
||||
<input type="text" id="downloadFileNameInput" class="form-control" data-i18n-placeholder="filename"
|
||||
placeholder="Filename" />
|
||||
<div style="margin-top: 15px; text-align: right;">
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary"
|
||||
onclick="document.getElementById('downloadFileModal').style.display = 'none';"
|
||||
data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary" onclick="confirmSingleDownload()"
|
||||
data-i18n-key="download">Download</button>
|
||||
<button id="cancelDownloadFile" class="btn btn-secondary" data-i18n-key="cancel">Cancel</button>
|
||||
<button id="confirmSingleDownloadButton" class="btn btn-primary" data-i18n-key="download">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -437,12 +437,26 @@ function initAuth() {
|
||||
submitLogin(formData);
|
||||
});
|
||||
}
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("/api/auth/logout.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRF-Token": window.csrfToken }
|
||||
}).then(() => window.location.reload(true)).catch(() => { });
|
||||
// handle ?logout=1 query
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('logout') === '1') {
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("userTOTPEnabled");
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
resetUserForm();
|
||||
|
||||
@@ -193,10 +193,10 @@ export function handleExtractZipSelected(e) {
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const zipNameModal = document.getElementById("downloadZipModal");
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
const zipNameModal = document.getElementById("downloadZipModal");
|
||||
const progressModal = document.getElementById("downloadProgressModal");
|
||||
const cancelZipBtn = document.getElementById("cancelDownloadZip");
|
||||
const confirmZipBtn = document.getElementById("confirmDownloadZip");
|
||||
|
||||
// 1) Cancel button hides the name modal
|
||||
if (cancelZipBtn) {
|
||||
@@ -219,8 +219,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
// b) Hide the name‐input modal, show the spinner modal
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
zipNameModal.style.display = "none";
|
||||
progressModal.style.display = "block";
|
||||
|
||||
// c) (Optional) update the “Preparing…” text if you gave it an ID
|
||||
const titleEl = document.getElementById("downloadProgressTitle");
|
||||
@@ -233,11 +233,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || "root",
|
||||
files: window.filesToDownload
|
||||
files: window.filesToDownload
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -252,8 +252,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// e) Hand off to the browser’s download manager
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
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;
|
||||
Reference in New Issue
Block a user