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

@@ -73,7 +73,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div style="display: flex; gap: 0.5rem; align-items: center;">
<input type="text" id="newKeyValue" readonly
style="flex: 1; padding: 0.75rem; font-family: var(--font-mono); font-size: 0.85rem; background: var(--bg-primary); border: 2px solid var(--terminal-green); color: var(--terminal-green);">
<button onclick="copyApiKey()" class="btn" title="Copy to clipboard">Copy</button>
<button data-action="copy-api-key" class="btn" title="Copy to clipboard">Copy</button>
</div>
</div>
@@ -135,7 +135,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</td>
<td>
<?php if ($key['is_active']): ?>
<button onclick="revokeKey(<?php echo $key['api_key_id']; ?>)" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">
<button data-action="revoke-key" data-id="<?php echo $key['api_key_id']; ?>" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.8rem;">
Revoke
</button>
<?php else: ?>
@@ -162,6 +162,22 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<script nonce="<?php echo $nonce; ?>">
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'copy-api-key':
copyApiKey();
break;
case 'revoke-key':
revokeKey(target.dataset.id);
break;
}
});
document.getElementById('generateKeyForm').addEventListener('submit', async function(e) {
e.preventDefault();

View File

@@ -41,7 +41,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Custom Field Definitions</h2>
<button onclick="showCreateModal()" class="btn">+ New Field</button>
<button data-action="show-create-modal" class="btn">+ New Field</button>
</div>
<table style="width: 100%;">
@@ -79,8 +79,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</span>
</td>
<td>
<button onclick="editField(<?php echo $field['field_id']; ?>)" class="btn btn-small">Edit</button>
<button onclick="deleteField(<?php echo $field['field_id']; ?>)" class="btn btn-small btn-danger">Delete</button>
<button data-action="edit-field" data-id="<?php echo $field['field_id']; ?>" class="btn btn-small">Edit</button>
<button data-action="delete-field" data-id="<?php echo $field['field_id']; ?>" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
@@ -92,13 +92,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="fieldModal" style="display: none;">
<div class="settings-modal" id="fieldModal" style="display: none;" data-action="close-modal-backdrop">
<div class="settings-content" style="max-width: 500px;">
<div class="settings-header">
<h3 id="modalTitle">Create Custom Field</h3>
<button class="close-settings" onclick="closeModal()">×</button>
<button class="close-settings" data-action="close-modal">×</button>
</div>
<form id="fieldForm" onsubmit="saveField(event)">
<form id="fieldForm">
<input type="hidden" id="field_id" name="field_id">
<div class="settings-body">
<div class="setting-row">
@@ -111,7 +111,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="setting-row">
<label for="field_type">Field Type *</label>
<select id="field_type" name="field_type" required onchange="toggleOptionsField()">
<select id="field_type" name="field_type" required data-action="toggle-options-field">
<option value="text">Text</option>
<option value="textarea">Text Area</option>
<option value="select">Dropdown (Select)</option>
@@ -148,7 +148,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-secondary" data-action="close-modal">Cancel</button>
</div>
</form>
</div>
@@ -169,16 +169,48 @@ $nonce = SecurityHeadersMiddleware::getNonce();
document.getElementById('fieldModal').style.display = 'none';
}
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'close-modal':
closeModal();
break;
case 'close-modal-backdrop':
if (event.target === target) closeModal();
break;
case 'edit-field':
editField(target.dataset.id);
break;
case 'delete-field':
deleteField(target.dataset.id);
break;
}
});
// Close modal when clicking on backdrop (outside content)
document.getElementById('fieldModal').addEventListener('click', (e) => {
if (e.target.classList.contains('settings-modal')) {
document.addEventListener('change', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
if (target.dataset.action === 'toggle-options-field') {
toggleOptionsField();
}
});
// Form submit handler
document.getElementById('fieldForm').addEventListener('submit', function(e) {
saveField(e);
});
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});

View File

@@ -41,7 +41,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Scheduled Tickets</h2>
<button onclick="showCreateModal()" class="btn">+ New Recurring Ticket</button>
<button data-action="show-create-modal" class="btn">+ New Recurring Ticket</button>
</div>
<table style="width: 100%;">
@@ -91,11 +91,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</span>
</td>
<td>
<button onclick="editRecurring(<?php echo $rt['recurring_id']; ?>)" class="btn btn-small">Edit</button>
<button onclick="toggleRecurring(<?php echo $rt['recurring_id']; ?>)" class="btn btn-small">
<button data-action="edit-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">Edit</button>
<button data-action="toggle-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small">
<?php echo $rt['is_active'] ? 'Disable' : 'Enable'; ?>
</button>
<button onclick="deleteRecurring(<?php echo $rt['recurring_id']; ?>)" class="btn btn-small btn-danger">Delete</button>
<button data-action="delete-recurring" data-id="<?php echo $rt['recurring_id']; ?>" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
@@ -107,13 +107,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="recurringModal" style="display: none;">
<div class="settings-modal" id="recurringModal" style="display: none;" data-action="close-modal-backdrop">
<div class="settings-content" style="max-width: 800px; width: 90%;">
<div class="settings-header">
<h3 id="modalTitle">Create Recurring Ticket</h3>
<button class="close-settings" onclick="closeModal()">×</button>
<button class="close-settings" data-action="close-modal">×</button>
</div>
<form id="recurringForm" onsubmit="saveRecurring(event)">
<form id="recurringForm">
<input type="hidden" id="recurring_id" name="recurring_id">
<div class="settings-body">
<div class="setting-row">
@@ -126,7 +126,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="setting-row">
<label for="schedule_type">Schedule Type *</label>
<select id="schedule_type" name="schedule_type" required onchange="updateScheduleOptions()">
<select id="schedule_type" name="schedule_type" required data-action="update-schedule-options">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
@@ -183,7 +183,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-secondary" data-action="close-modal">Cancel</button>
</div>
</form>
</div>
@@ -203,16 +203,51 @@ $nonce = SecurityHeadersMiddleware::getNonce();
document.getElementById('recurringModal').style.display = 'none';
}
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'close-modal':
closeModal();
break;
case 'close-modal-backdrop':
if (event.target === target) closeModal();
break;
case 'edit-recurring':
editRecurring(target.dataset.id);
break;
case 'toggle-recurring':
toggleRecurring(target.dataset.id);
break;
case 'delete-recurring':
deleteRecurring(target.dataset.id);
break;
}
});
// Close modal when clicking on backdrop (outside content)
document.getElementById('recurringModal').addEventListener('click', (e) => {
if (e.target.classList.contains('settings-modal')) {
document.addEventListener('change', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
if (target.dataset.action === 'update-schedule-options') {
updateScheduleOptions();
}
});
// Form submit handler
document.getElementById('recurringForm').addEventListener('submit', function(e) {
saveRecurring(e);
});
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});

View File

@@ -41,7 +41,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Ticket Templates</h2>
<button onclick="showCreateModal()" class="btn">+ New Template</button>
<button data-action="show-create-modal" class="btn">+ New Template</button>
</div>
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
@@ -79,8 +79,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</span>
</td>
<td>
<button onclick="editTemplate(<?php echo $tpl['template_id']; ?>)" class="btn btn-small">Edit</button>
<button onclick="deleteTemplate(<?php echo $tpl['template_id']; ?>)" class="btn btn-small btn-danger">Delete</button>
<button data-action="edit-template" data-id="<?php echo $tpl['template_id']; ?>" class="btn btn-small">Edit</button>
<button data-action="delete-template" data-id="<?php echo $tpl['template_id']; ?>" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
@@ -92,13 +92,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="templateModal" style="display: none;">
<div class="settings-modal" id="templateModal" style="display: none;" data-action="close-modal-backdrop">
<div class="settings-content" style="max-width: 800px; width: 90%;">
<div class="settings-header">
<h3 id="modalTitle">Create Template</h3>
<button class="close-settings" onclick="closeModal()">×</button>
<button class="close-settings" data-action="close-modal">×</button>
</div>
<form id="templateForm" onsubmit="saveTemplate(event)">
<form id="templateForm">
<input type="hidden" id="template_id" name="template_id">
<div class="settings-body">
<div class="setting-row">
@@ -154,7 +154,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-secondary" data-action="close-modal">Cancel</button>
</div>
</form>
</div>
@@ -176,16 +176,39 @@ $nonce = SecurityHeadersMiddleware::getNonce();
document.getElementById('templateModal').style.display = 'none';
}
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'close-modal':
closeModal();
break;
case 'close-modal-backdrop':
if (event.target === target) closeModal();
break;
case 'edit-template':
editTemplate(target.dataset.id);
break;
case 'delete-template':
deleteTemplate(target.dataset.id);
break;
}
});
// Close modal when clicking on backdrop (outside content)
document.getElementById('templateModal').addEventListener('click', (e) => {
if (e.target.classList.contains('settings-modal')) {
// Form submit handler
document.getElementById('templateForm').addEventListener('submit', function(e) {
saveTemplate(e);
});
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});

View File

@@ -41,7 +41,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ascii-frame-inner">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Status Transitions</h2>
<button onclick="showCreateModal()" class="btn">+ New Transition</button>
<button data-action="show-create-modal" class="btn">+ New Transition</button>
</div>
<p style="color: var(--terminal-green-dim); margin-bottom: 1rem;">
@@ -119,8 +119,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</span>
</td>
<td>
<button onclick="editTransition(<?php echo $wf['transition_id']; ?>)" class="btn btn-small">Edit</button>
<button onclick="deleteTransition(<?php echo $wf['transition_id']; ?>)" class="btn btn-small btn-danger">Delete</button>
<button data-action="edit-transition" data-id="<?php echo $wf['transition_id']; ?>" class="btn btn-small">Edit</button>
<button data-action="delete-transition" data-id="<?php echo $wf['transition_id']; ?>" class="btn btn-small btn-danger">Delete</button>
</td>
</tr>
<?php endforeach; ?>
@@ -132,13 +132,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<!-- Create/Edit Modal -->
<div class="settings-modal" id="workflowModal" style="display: none;">
<div class="settings-modal" id="workflowModal" style="display: none;" data-action="close-modal-backdrop">
<div class="settings-content" style="max-width: 450px;">
<div class="settings-header">
<h3 id="modalTitle">Create Transition</h3>
<button class="close-settings" onclick="closeModal()">×</button>
<button class="close-settings" data-action="close-modal">×</button>
</div>
<form id="workflowForm" onsubmit="saveTransition(event)">
<form id="workflowForm">
<input type="hidden" id="transition_id" name="transition_id">
<div class="settings-body">
<div class="setting-row">
@@ -171,7 +171,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div>
<div class="settings-footer">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-secondary" data-action="close-modal">Cancel</button>
</div>
</form>
</div>
@@ -193,16 +193,39 @@ $nonce = SecurityHeadersMiddleware::getNonce();
document.getElementById('workflowModal').style.display = 'none';
}
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
// Event delegation for data-action handlers
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
switch (action) {
case 'show-create-modal':
showCreateModal();
break;
case 'close-modal':
closeModal();
break;
case 'close-modal-backdrop':
if (event.target === target) closeModal();
break;
case 'edit-transition':
editTransition(target.dataset.id);
break;
case 'delete-transition':
deleteTransition(target.dataset.id);
break;
}
});
// Close modal when clicking on backdrop (outside content)
document.getElementById('workflowModal').addEventListener('click', (e) => {
if (e.target.classList.contains('settings-modal')) {
// Form submit handler
document.getElementById('workflowForm').addEventListener('submit', function(e) {
saveTransition(e);
});
// Close modal on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});