Harden CSP by removing unsafe-inline for scripts

Refactored all inline event handlers (onclick, onchange, onsubmit) to use
addEventListener with data-action attributes and event delegation pattern.

Changes:
- views/*.php: Replaced inline handlers with data-action attributes
- views/admin/*.php: Same refactoring for all admin views
- assets/js/dashboard.js: Added event delegation for bulk/quick action modals
- assets/js/ticket.js: Added event delegation for dynamic elements
- assets/js/markdown.js: Refactored toolbar button handlers
- assets/js/keyboard-shortcuts.js: Refactored modal close button
- SecurityHeadersMiddleware.php: Enabled strict CSP with nonces

The CSP now uses script-src 'self' 'nonce-{nonce}' instead of 'unsafe-inline',
significantly improving XSS protection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:15:55 -05:00
parent 37be81b3e2
commit c3f7593f3c
13 changed files with 564 additions and 158 deletions

View File

@@ -569,7 +569,7 @@ function showTab(tabName) {
// Show selected tab and activate its button
document.getElementById(`${tabName}-tab`).style.display = 'block';
document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active');
document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
// Load attachments when tab is shown
if (tabName === 'attachments') {
@@ -654,7 +654,7 @@ function renderDependencies(dependencies) {
<span style="margin-left: 0.5rem;">${escapeHtml(dep.title)}</span>
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${escapeHtml(dep.status)}</span>
</div>
<button onclick="removeDependency('${dep.dependency_id}')" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
<button data-action="remove-dependency" data-dependency-id="${dep.dependency_id}" class="btn btn-small" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">Remove</button>
</div>`;
});
@@ -956,7 +956,7 @@ function renderAttachments(attachments) {
</div>
<div class="attachment-actions">
<a href="/api/download_attachment.php?id=${att.attachment_id}" class="btn btn-small" title="Download">⬇</a>
<button onclick="deleteAttachment(${att.attachment_id})" class="btn btn-small btn-danger" title="Delete">✕</button>
<button data-action="delete-attachment" data-attachment-id="${att.attachment_id}" class="btn btn-small btn-danger" title="Delete">✕</button>
</div>
</div>`;
});
@@ -1150,7 +1150,7 @@ function showMentionSuggestions(query, textarea) {
let html = '';
filtered.forEach((user, index) => {
const isSelected = index === 0 ? 'selected' : '';
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" onclick="selectMention('${escapeHtml(user.username)}')">
html += `<div class="mention-option ${isSelected}" data-username="${escapeHtml(user.username)}" data-action="select-mention">
<span class="mention-username">@${escapeHtml(user.username)}</span>
${user.display_name ? `<span class="mention-displayname">${escapeHtml(user.display_name)}</span>` : ''}
</div>`;
@@ -1212,6 +1212,31 @@ document.addEventListener('DOMContentLoaded', function() {
el.innerHTML = highlightMentions(el.innerHTML);
}
});
// Event delegation for dynamically created elements
document.addEventListener('click', function(e) {
const target = e.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'remove-dependency':
removeDependency(target.dataset.dependencyId);
break;
case 'delete-attachment':
deleteAttachment(target.dataset.attachmentId);
break;
case 'select-mention':
selectMention(target.dataset.username);
break;
case 'save-edit-comment':
saveEditComment(parseInt(target.dataset.commentId));
break;
case 'cancel-edit-comment':
cancelEditComment(parseInt(target.dataset.commentId));
break;
}
});
});
// ========================================
@@ -1251,8 +1276,8 @@ function editComment(commentId) {
Markdown
</label>
<div class="comment-edit-buttons">
<button type="button" class="btn btn-small" onclick="saveEditComment(${commentId})">Save</button>
<button type="button" class="btn btn-secondary btn-small" onclick="cancelEditComment(${commentId})">Cancel</button>
<button type="button" class="btn btn-small" data-action="save-edit-comment" data-comment-id="${commentId}">Save</button>
<button type="button" class="btn btn-secondary btn-small" data-action="cancel-edit-comment" data-comment-id="${commentId}">Cancel</button>
</div>
</div>
`;