Harden security: CORS, XSS, rate limiting, CSP, SRI

- Lock down CORS to ALLOWED_ORIGINS env var (was wildcard)
- Fix admin panel XSS: use data-username attributes instead of
  interpolating usernames into onclick handlers
- Add rate limiting to /api/auth/register (3r/m) and /api/admin/*
  (10r/m); set limit_req_status 429
- Add Content-Security-Policy header restricting scripts to self
  and cdn.jsdelivr.net
- Add Subresource Integrity hash to Chart.js CDN script tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 23:18:33 -07:00
parent 958c409e8e
commit f6cc7a606e
4 changed files with 57 additions and 15 deletions

View File

@@ -47,20 +47,20 @@ function renderUsers(users) {
: `<button class="btn btn-sm btn-ghost" onclick="toggleUser(${u.id}, true)" ${isSelf ? 'disabled title="Cannot disable yourself"' : ''}>Disable</button>`;
const impersonateBtn = !isSelf
? `<button class="btn btn-sm btn-ghost" onclick="impersonateUser(${u.id}, '${u.username}')">Login As</button>`
? `<button class="btn btn-sm btn-ghost" onclick="impersonateUser(${u.id})">Login As</button>`
: '';
const deleteBtn = !isSelf
? `<button class="btn btn-sm btn-danger" onclick="showDeleteModal(${u.id}, '${u.username}')">Delete</button>`
? `<button class="btn btn-sm btn-danger" data-username="${escHtml(u.username)}" onclick="showDeleteModal(${u.id}, this.dataset.username)">Delete</button>`
: '';
return `<tr>
<td><strong>${u.username}</strong></td>
<td><strong>${escHtml(u.username)}</strong></td>
<td>${roleLabel}</td>
<td>${statusLabel}</td>
<td>${created}</td>
<td class="actions" style="display:flex;gap:0.35rem;flex-wrap:wrap">
<button class="btn btn-sm btn-ghost" onclick="showResetModal(${u.id}, '${u.username}')">Reset PW</button>
<button class="btn btn-sm btn-ghost" data-username="${escHtml(u.username)}" onclick="showResetModal(${u.id}, this.dataset.username)">Reset PW</button>
${toggleBtn}
${impersonateBtn}
${deleteBtn}
@@ -115,7 +115,7 @@ async function toggleUser(id, disable) {
}
}
async function impersonateUser(id, username) {
async function impersonateUser(id) {
try {
const data = await API.post(`/api/admin/users/${id}/impersonate`, {});
// Save admin token so user can return