Fix CSP blocking inline handlers - add unsafe-inline fallback

- Refactored TicketView.php to use event listeners instead of onclick
- Added unsafe-inline to CSP as fallback for legacy handlers in other views
- TODO: Complete refactoring of DashboardView and admin views

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 10:42:09 -05:00
parent 674a427edb
commit 55209e0b05
2 changed files with 122 additions and 26 deletions

View File

@@ -26,8 +26,10 @@ class SecurityHeadersMiddleware {
$nonce = self::getNonce(); $nonce = self::getNonce();
// Content Security Policy - restricts where resources can be loaded from // Content Security Policy - restricts where resources can be loaded from
// Using nonce for inline scripts instead of unsafe-inline for better security // Nonces are used for <script> tags, but 'unsafe-inline' is needed for legacy onclick handlers
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';"); // TODO: Refactor all inline event handlers (onclick, etc.) to use addEventListener,
// then remove 'unsafe-inline' from script-src for full CSP protection
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'nonce-{$nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';");
// Prevent clickjacking by disallowing framing // Prevent clickjacking by disallowing framing
header("X-Frame-Options: DENY"); header("X-Frame-Options: DENY");

View File

@@ -87,7 +87,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<?php if ($GLOBALS['currentUser']['is_admin']): ?> <?php if ($GLOBALS['currentUser']['is_admin']): ?>
<span class="admin-badge">Admin</span> <span class="admin-badge">Admin</span>
<?php endif; ?> <?php endif; ?>
<button class="settings-icon" title="Settings (Alt+S)" onclick="openSettingsModal()">⚙</button> <button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn">⚙</button>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
@@ -226,7 +226,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<button id="editButton" class="btn" onclick="toggleEditMode()">Edit Ticket</button> <button id="editButton" class="btn">Edit Ticket</button>
</div> </div>
</div> </div>
</div> </div>
@@ -240,11 +240,11 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<div class="ascii-section-header">Content Sections</div> <div class="ascii-section-header">Content Sections</div>
<div class="ascii-content"> <div class="ascii-content">
<div class="ticket-tabs"> <div class="ticket-tabs">
<button class="tab-btn active" onclick="showTab('description')">Description</button> <button class="tab-btn active" data-tab="description">Description</button>
<button class="tab-btn" onclick="showTab('comments')">Comments</button> <button class="tab-btn" data-tab="comments">Comments</button>
<button class="tab-btn" onclick="showTab('attachments')">Attachments</button> <button class="tab-btn" data-tab="attachments">Attachments</button>
<button class="tab-btn" onclick="showTab('dependencies')">Dependencies</button> <button class="tab-btn" data-tab="dependencies">Dependencies</button>
<button class="tab-btn" onclick="showTab('activity')">Activity</button> <button class="tab-btn" data-tab="activity">Activity</button>
</div> </div>
</div> </div>
@@ -288,7 +288,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<span class="toggle-label">Preview Markdown</span> <span class="toggle-label">Preview Markdown</span>
</div> </div>
</div> </div>
<button onclick="addComment()" class="btn">Add Comment</button> <button id="addCommentBtn" class="btn">Add Comment</button>
</div> </div>
<div id="markdownPreview" class="markdown-preview" style="display: none;"></div> <div id="markdownPreview" class="markdown-preview" style="display: none;"></div>
</div> </div>
@@ -320,8 +320,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
// Edit/Delete buttons for owner or admin // Edit/Delete buttons for owner or admin
if ($canModify) { if ($canModify) {
echo "<div class='comment-actions'>"; echo "<div class='comment-actions'>";
echo "<button type='button' class='comment-action-btn edit-btn' onclick='editComment({$commentId})' title='Edit comment'>✏️</button>"; echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit comment'>✏️</button>";
echo "<button type='button' class='comment-action-btn delete-btn' onclick='deleteComment({$commentId})' title='Delete comment'>🗑️</button>"; echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete comment'>🗑️</button>";
echo "</div>"; echo "</div>";
} }
@@ -357,7 +357,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<p>Drag and drop files here or click to browse</p> <p>Drag and drop files here or click to browse</p>
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p> <p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
<input type="file" id="fileInput" multiple style="display: none;"> <input type="file" id="fileInput" multiple style="display: none;">
<button type="button" onclick="document.getElementById('fileInput').click();" class="btn" style="margin-top: 1rem;">Browse Files</button> <button type="button" id="browseFilesBtn" class="btn" style="margin-top: 1rem;">Browse Files</button>
</div> </div>
</div> </div>
<div id="uploadProgress" style="display: none; margin-top: 1rem;"> <div id="uploadProgress" style="display: none; margin-top: 1rem;">
@@ -393,7 +393,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
<option value="relates_to">Relates To</option> <option value="relates_to">Relates To</option>
<option value="duplicates">Duplicates</option> <option value="duplicates">Duplicates</option>
</select> </select>
<button onclick="addDependency()" class="btn">Add</button> <button id="addDependencyBtn" class="btn">Add</button>
</div> </div>
</div> </div>
@@ -447,30 +447,124 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div> </div>
<!-- END OUTER FRAME --> <!-- END OUTER FRAME -->
<script nonce="<?php echo $nonce; ?>"> <script nonce="<?php echo $nonce; ?>">
// Initialize the ticket view // Initialize the ticket view and attach event listeners (CSP-compliant)
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Ticket data alias for compatibility
window.ticketData.id = window.ticketData.ticket_id;
// Initialize with description tab
if (typeof showTab === 'function') { if (typeof showTab === 'function') {
showTab('description'); showTab('description');
} else { }
console.error('showTab function not defined');
// Tab buttons - use event delegation
document.querySelectorAll('.tab-btn[data-tab]').forEach(function(btn) {
btn.addEventListener('click', function() {
var tab = this.getAttribute('data-tab');
if (typeof showTab === 'function') {
showTab(tab);
}
});
});
// Settings button
var settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
settingsBtn.addEventListener('click', function() {
if (typeof openSettingsModal === 'function') openSettingsModal();
});
}
// Edit button
var editBtn = document.getElementById('editButton');
if (editBtn) {
editBtn.addEventListener('click', function() {
if (typeof toggleEditMode === 'function') toggleEditMode();
});
}
// Add comment button
var addCommentBtn = document.getElementById('addCommentBtn');
if (addCommentBtn) {
addCommentBtn.addEventListener('click', function() {
if (typeof addComment === 'function') addComment();
});
}
// Comment edit/delete buttons - use event delegation
document.addEventListener('click', function(e) {
var target = e.target.closest('[data-action]');
if (!target) return;
var action = target.getAttribute('data-action');
var commentId = target.getAttribute('data-comment-id');
if (action === 'edit-comment' && commentId) {
if (typeof editComment === 'function') editComment(parseInt(commentId));
} else if (action === 'delete-comment' && commentId) {
if (typeof deleteComment === 'function') deleteComment(parseInt(commentId));
}
});
// Browse files button
var browseFilesBtn = document.getElementById('browseFilesBtn');
if (browseFilesBtn) {
browseFilesBtn.addEventListener('click', function() {
document.getElementById('fileInput').click();
});
}
// Add dependency button
var addDepBtn = document.getElementById('addDependencyBtn');
if (addDepBtn) {
addDepBtn.addEventListener('click', function() {
if (typeof addDependency === 'function') addDependency();
});
}
// Settings modal buttons
var closeSettingsBtn = document.getElementById('closeSettingsBtn');
if (closeSettingsBtn) {
closeSettingsBtn.addEventListener('click', function() {
if (typeof closeSettingsModal === 'function') closeSettingsModal();
});
}
var saveSettingsBtn = document.getElementById('saveSettingsBtn');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', function() {
if (typeof saveSettings === 'function') saveSettings();
});
}
var cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
if (cancelSettingsBtn) {
cancelSettingsBtn.addEventListener('click', function() {
if (typeof closeSettingsModal === 'function') closeSettingsModal();
});
}
// Settings modal backdrop click
var settingsModal = document.getElementById('settingsModal');
if (settingsModal) {
settingsModal.addEventListener('click', function(e) {
if (e.target.classList.contains('settings-modal')) {
if (typeof closeSettingsModal === 'function') closeSettingsModal();
}
});
} }
}); });
</script> </script>
<script nonce="<?php echo $nonce; ?>">
// Ticket data already initialized in head, add id alias for compatibility
window.ticketData.id = window.ticketData.ticket_id;
console.log('Ticket data loaded:', window.ticketData);
</script>
<!-- Settings Modal (same as dashboard) --> <!-- Settings Modal (same as dashboard) -->
<div class="settings-modal" id="settingsModal" style="display: none;" onclick="closeOnBackdropClick(event)"> <div class="settings-modal" id="settingsModal" style="display: none;">
<div class="settings-content"> <div class="settings-content">
<span class="bottom-left-corner">╚</span> <span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span> <span class="bottom-right-corner">╝</span>
<div class="settings-header"> <div class="settings-header">
<h3>⚙ System Preferences</h3> <h3>⚙ System Preferences</h3>
<button class="close-settings" onclick="closeSettingsModal()">✗</button> <button class="close-settings" id="closeSettingsBtn">✗</button>
</div> </div>
<div class="settings-body"> <div class="settings-body">
@@ -592,8 +686,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
</div> </div>
<div class="settings-footer"> <div class="settings-footer">
<button class="btn btn-primary" onclick="saveSettings()">Save Preferences</button> <button class="btn btn-primary" id="saveSettingsBtn">Save Preferences</button>
<button class="btn btn-secondary" onclick="closeSettingsModal()">Cancel</button> <button class="btn btn-secondary" id="cancelSettingsBtn">Cancel</button>
</div> </div>
</div> </div>
</div> </div>