fix(csp): remove all inline styles, scripts, and event handlers for strict CSP compliance

Replace ~50 inline style="" attributes with CSS classes, move 3 inline
<script> blocks to external JS files, replace all inline event handlers
(onclick, onerror) with addEventListener, and remove createElement('style')
from icons.js. All changes support the strict CSP policy (style-src 'self';
script-src 'self') without weakening it.
This commit is contained in:
Jared Wolff
2026-03-05 16:18:30 -05:00
parent f2d35ca792
commit c08926b817
28 changed files with 397 additions and 212 deletions
+24 -6
View File
@@ -124,17 +124,35 @@ async function loadUsers() {
'<td><span class="badge badge-' + escapeHtml(u.role) + '">' + (u.role === 'admin' ? '<i class="fas fa-shield-alt badge-admin-icon-small"></i> ' : '') + escapeHtml(u.role) + '</span></td>' +
'<td>' + authBadge + '</td>' +
'<td><span class="badge badge-' + (u.active ? 'active' : 'inactive') + '">' + (u.active ? 'Active' : 'Inactive') + '</span></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" style="width:' + Math.min(quotaPct, 100) + '%"></div></div><span class="quota-text">' + quotaText + '</span></div></td>' +
'<td><div class="quota-bar"><div class="progress-bar quota-progress-fixed"><div class="progress-fill ' + quotaColor + '" data-width="' + Math.min(quotaPct, 100) + '"></div></div><span class="quota-text">' + quotaText + '</span></div></td>' +
'<td class="user-last-login-cell">' + timeAgo(u.last_login_at) + '</td>' +
'<td><div class="actions-row">' +
'<button class="btn btn-sm btn-secondary" onclick="openQuotaModal(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\',' + u.storage_quota_bytes + ')" title="Edit quota"><i class="fas fa-box"></i></button>' +
(isOidc ? '' : '<button class="btn btn-sm btn-secondary" onclick="openResetPasswordModal(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\')" title="Reset password"><i class="fas fa-key"></i></button>') +
'<button class="btn btn-sm btn-secondary" onclick="toggleRole(\'' + _escJs(u.id) + '\',\'' + _escJs(u.role) + '\')" title="Toggle role"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + '" onclick="toggleActive(\'' + _escJs(u.id) + '\',' + u.active + ')" title="' + (u.active ? 'Deactivate' : 'Activate') + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteUser(\'' + _escJs(u.id) + '\',\'' + _escJs(u.username) + '\')" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="quota" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" data-quota="' + u.storage_quota_bytes + '" title="Edit quota"><i class="fas fa-box"></i></button>' +
(isOidc ? '' : '<button class="btn btn-sm btn-secondary admin-action-btn" data-action="reset-pw" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="Reset password"><i class="fas fa-key"></i></button>') +
'<button class="btn btn-sm btn-secondary admin-action-btn" data-action="toggle-role" data-uid="' + _escJs(u.id) + '" data-role="' + _escJs(u.role) + '" title="Toggle role"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-' + (u.role === 'admin' ? 'user' : 'crown') + '"></i></button>' +
'<button class="btn btn-sm ' + (u.active ? 'btn-danger' : 'btn-success') + ' admin-action-btn" data-action="toggle-active" data-uid="' + _escJs(u.id) + '" data-active="' + u.active + '" title="' + (u.active ? 'Deactivate' : 'Activate') + '"' + (isSelf && u.active ? ' disabled' : '') + '><i class="fas fa-' + (u.active ? 'ban' : 'check') + '"></i></button>' +
'<button class="btn btn-sm btn-danger admin-action-btn" data-action="delete" data-uid="' + _escJs(u.id) + '" data-uname="' + _escJs(u.username) + '" title="Delete"' + (isSelf ? ' disabled' : '') + '><i class="fas fa-trash-alt"></i></button>' +
'</div></td></tr>';
}).join('');
// Set dynamic progress bar widths (CSP-safe via JS property)
document.querySelectorAll('.progress-fill[data-width]').forEach(function(el) {
el.style.width = el.dataset.width + '%';
el.removeAttribute('data-width');
});
// Wire up admin action buttons (replaces inline onclick handlers)
document.querySelectorAll('.admin-action-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var action = btn.dataset.action;
if (action === 'quota') openQuotaModal(btn.dataset.uid, btn.dataset.uname, Number(btn.dataset.quota));
else if (action === 'reset-pw') openResetPasswordModal(btn.dataset.uid, btn.dataset.uname);
else if (action === 'toggle-role') toggleRole(btn.dataset.uid, btn.dataset.role);
else if (action === 'toggle-active') toggleActive(btn.dataset.uid, btn.dataset.active === 'true');
else if (action === 'delete') deleteUser(btn.dataset.uid, btn.dataset.uname);
});
});
document.getElementById('users-info').textContent = 'Showing ' + (usersPage * PAGE_SIZE + 1) + '-' + Math.min((usersPage + 1) * PAGE_SIZE, totalUsers) + ' of ' + totalUsers;
document.getElementById('prev-btn').disabled = usersPage === 0;
document.getElementById('next-btn').disabled = (usersPage + 1) * PAGE_SIZE >= totalUsers;
+33
View File
@@ -0,0 +1,33 @@
// Read error type from URL query parameter
var params = new URLSearchParams(window.location.search);
var errorType = params.get('type') || 'generic';
var errorTitle = document.getElementById('error-title');
var errorMessage = document.getElementById('error-message');
var errorAction = document.getElementById('error-action');
switch(errorType) {
case 'invalid-credentials':
errorTitle.textContent = 'Login Failed';
errorMessage.textContent = 'Invalid username or password. Please check your credentials and try again.';
errorAction.textContent = 'Try Again';
errorAction.addEventListener('click', function() { history.back(); });
break;
case 'session-expired':
errorTitle.textContent = 'Session Expired';
errorMessage.textContent = 'Your session has expired. Please try again.';
errorAction.textContent = 'Close Window';
errorAction.addEventListener('click', function() { window.close(); });
break;
case 'not-found':
errorTitle.textContent = 'Not Found';
errorMessage.textContent = 'The requested page was not found.';
errorAction.textContent = 'Close Window';
errorAction.addEventListener('click', function() { window.close(); });
break;
default:
errorTitle.textContent = 'Error';
errorMessage.textContent = 'An unexpected error occurred. Please try again.';
errorAction.textContent = 'Close Window';
errorAction.addEventListener('click', function() { window.close(); });
}
+38
View File
@@ -0,0 +1,38 @@
// Extract token from URL path and set form action
var pathParts = window.location.pathname.split('/');
var token = pathParts[pathParts.length - 1];
// Validate token is hex-only to prevent injection
if (!/^[0-9a-fA-F]+$/.test(token)) {
document.body.innerHTML = '<p>Invalid session token.</p>';
throw new Error('Invalid token format');
}
document.getElementById('login-flow-form').action = '/login/v2/flow/' + token;
// Check if OIDC is available and configure SSO button
(async function() {
try {
var resp = await fetch('/api/auth/oidc/providers');
if (!resp.ok) return;
var info = await resp.json();
if (!info.enabled) return;
// Show OIDC section
document.getElementById('oidc-section').classList.remove('hidden');
// Update button text with provider name
var btn = document.getElementById('oidc-button');
btn.textContent = 'Sign in with ' + (info.provider_name || 'SSO');
// If password login is disabled, hide the password form
if (!info.password_login_enabled) {
document.getElementById('login-flow-form').style.display = 'none';
}
// SSO button redirects to the OIDC flow for this NC token
btn.addEventListener('click', function() {
window.location.href = '/login/v2/flow/' + token + '/oidc';
});
} catch(e) {
// OIDC not available — silently keep password-only mode
}
})();
+7
View File
@@ -0,0 +1,7 @@
document.getElementById('close-window-btn').addEventListener('click', function() {
window.close();
});
// Auto-close after 3 seconds
setTimeout(function() {
window.close();
}, 3000);
+26 -17
View File
@@ -62,7 +62,7 @@ async function init() {
document.getElementById('p-storage-text').textContent = formatBytes(used) + ' / ' + (quota > 0 ? formatBytes(quota) : 'Unlimited');
if (user.auth_provider && user.auth_provider !== 'local') {
document.getElementById('password-section').style.display = 'none';
document.getElementById('password-section').classList.add('hidden');
}
loadAppPasswords();
@@ -72,7 +72,7 @@ async function init() {
if (oidcResp.ok) {
const oidcInfo = await oidcResp.json();
if (!oidcInfo.password_login_enabled) {
document.getElementById('password-section').style.display = 'none';
document.getElementById('password-section').classList.add('hidden');
}
}
} catch (oidcErr) {
@@ -157,7 +157,7 @@ function renderPwRow(pw) {
btn.className = 'btn btn-danger-sm';
btn.innerHTML = '<i class="fas fa-trash"></i>';
btn.title = 'Revoke';
btn.onclick = function () { revokeAppPassword(pw.id, pw.label); };
btn.addEventListener('click', function () { revokeAppPassword(pw.id, pw.label); });
actions.appendChild(btn);
tr.append(label, created, lastUsed, actions);
return tr;
@@ -167,7 +167,7 @@ async function loadAppPasswords() {
try {
const resp = await fetch(API + '/auth/app-passwords', { headers: headers() });
if (!resp.ok) {
document.getElementById('app-passwords-section').style.display = 'none';
document.getElementById('app-passwords-section').classList.add('hidden');
return;
}
const passwords = await resp.json();
@@ -180,20 +180,20 @@ async function loadAppPasswords() {
const empty = document.getElementById('app-pw-empty');
tbody.innerHTML = '';
if (userPws.length === 0) {
table.style.display = 'none';
empty.style.display = 'block';
table.classList.add('hidden');
empty.classList.remove('hidden');
} else {
table.style.display = '';
empty.style.display = 'none';
table.classList.remove('hidden');
empty.classList.add('hidden');
for (const pw of userPws) tbody.appendChild(renderPwRow(pw));
}
// Auto-generated (client session) passwords
const autoSection = document.getElementById('app-pw-auto-section');
if (autoPws.length === 0) {
autoSection.style.display = 'none';
autoSection.classList.add('hidden');
} else {
autoSection.style.display = '';
autoSection.classList.remove('hidden');
document.getElementById('app-pw-auto-count').textContent = autoPws.length;
const autoTbody = document.getElementById('app-pw-auto-tbody');
autoTbody.innerHTML = '';
@@ -207,9 +207,9 @@ async function loadAppPasswords() {
function toggleAutoPasswords() {
const body = document.getElementById('app-pw-auto-body');
const chevron = document.getElementById('app-pw-auto-chevron');
const open = body.style.display === 'none';
body.style.display = open ? '' : 'none';
chevron.className = open ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
const isHidden = body.classList.contains('hidden');
body.classList.toggle('hidden', !isHidden);
chevron.className = isHidden ? 'fas fa-chevron-down' : 'fas fa-chevron-right';
}
async function createAppPassword() {
@@ -241,7 +241,7 @@ async function createAppPassword() {
const result = await resp.json();
document.getElementById('app-pw-created-label').textContent = result.label;
document.getElementById('app-pw-created-password').textContent = result.password;
document.getElementById('app-pw-created').style.display = 'block';
document.getElementById('app-pw-created').classList.remove('hidden');
labelInput.value = '';
loadAppPasswords();
} catch (err) {
@@ -255,7 +255,7 @@ async function createAppPassword() {
function copyAppPassword() {
const pw = document.getElementById('app-pw-created-password').textContent;
navigator.clipboard.writeText(pw).then(function () {
const btn = document.querySelector('.btn-copy');
const btn = document.getElementById('app-pw-copy-btn');
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(function () { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 1500);
});
@@ -269,7 +269,7 @@ async function revokeAppPassword(id, label) {
headers: headers()
});
if (resp.ok || resp.status === 204) {
document.getElementById('app-pw-created').style.display = 'none';
document.getElementById('app-pw-created').classList.add('hidden');
loadAppPasswords();
} else {
const err = await resp.json().catch(() => ({}));
@@ -280,7 +280,16 @@ async function revokeAppPassword(id, label) {
}
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
init();
/* Wire up form handler (replaces inline onsubmit) */
/* Wire up event handlers (replaces inline onclick/onsubmit) */
document.getElementById('password-form').addEventListener('submit', changePassword);
document.getElementById('app-pw-generate').addEventListener('click', createAppPassword);
document.getElementById('app-pw-copy-btn').addEventListener('click', copyAppPassword);
document.getElementById('app-pw-auto-toggle').addEventListener('click', toggleAutoPasswords);
+3 -3
View File
@@ -92,14 +92,14 @@ const sharedView = {
</div>
</div>
<div id="empty-shared-state" class="empty-state" style="display:none;">
<i class="fas fa-share-alt" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
<div id="empty-shared-state" class="empty-state hidden">
<i class="fas fa-share-alt empty-state-icon"></i>
<p data-i18n="shared_emptyStateTitle">No shared items</p>
<p data-i18n="shared_emptyStateDesc">Items you share will appear here</p>
<button id="go-to-files-btn" class="button primary" data-i18n="shared.goToFiles">Go to Files</button>
</div>
<div class="shared-list-container" style="display:none;">
<div class="shared-list-container hidden">
<table class="shared-table">
<thead>
<tr>