Security hardening and performance improvements

- Add visibility check to attachment downloads (prevents unauthorized access)
- Fix ticket ID collision with uniqueness verification loop
- Harden CSP: replace unsafe-inline with nonce-based script execution
- Add IP-based rate limiting (supplements session-based)
- Add visibility checks to bulk operations
- Validate internal visibility requires groups
- Optimize user activity query (JOINs vs subqueries)
- Update documentation with design decisions and security info

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 20:27:15 -05:00
parent a08390a500
commit fa40010287
17 changed files with 457 additions and 128 deletions

View File

@@ -1,5 +1,8 @@
<?php
// This file contains the HTML template for creating a new ticket
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -10,13 +13,10 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>">
// CSRF Token for AJAX requests
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -239,7 +239,7 @@
</div>
<!-- END OUTER FRAME -->
<script>
<script nonce="<?php echo $nonce; ?>">
// Duplicate detection with debounce
let duplicateCheckTimeout = null;

View File

@@ -1,6 +1,9 @@
<?php
// This file contains the HTML template for the dashboard
// It receives $tickets, $totalTickets, $totalPages, $page, $status, $categories, $types variables from the controller
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -10,16 +13,13 @@
<title>Ticket Dashboard</title>
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260124e"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ascii-banner.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>">
// CSRF Token for AJAX requests
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
// Timezone configuration (from server)
window.APP_TIMEZONE = '<?php echo $GLOBALS['config']['TIMEZONE']; ?>';
window.APP_TIMEZONE_OFFSET = <?php echo $GLOBALS['config']['TIMEZONE_OFFSET']; ?>; // minutes from UTC
@@ -32,7 +32,7 @@
<div id="boot-sequence" class="boot-overlay">
<pre id="boot-text"></pre>
</div>
<script>
<script nonce="<?php echo $nonce; ?>">
function showBootSequence() {
const bootText = document.getElementById('boot-text');
const bootOverlay = document.getElementById('boot-sequence');
@@ -108,7 +108,7 @@
</button>
<div id="ascii-banner-container" class="banner-content"></div>
</div>
<script>
<script nonce="<?php echo $nonce; ?>">
function toggleBanner() {
const wrapper = document.querySelector('.ascii-banner-wrapper');
const icon = document.querySelector('.toggle-icon');
@@ -764,10 +764,10 @@
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/advanced-search.js"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/keyboard-shortcuts.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/advanced-search.js"></script>
<script nonce="<?php echo $nonce; ?>">
// Admin dropdown toggle
function toggleAdminMenu(event) {
event.stopPropagation();

View File

@@ -38,6 +38,10 @@ function formatDetails($details, $actionType) {
}
return '';
}
<?php
require_once __DIR__ . '/../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -48,22 +52,19 @@ function formatDetails($details, $actionType) {
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css?v=20260126c">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css?v=20260124e">
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260124e"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js?v=20260124e"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/markdown.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/dashboard.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/ticket.js?v=20260124e"></script>
<script nonce="<?php echo $nonce; ?>">
// CSRF Token for AJAX requests
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
// Timezone configuration (from server)
window.APP_TIMEZONE = '<?php echo $GLOBALS['config']['TIMEZONE']; ?>';
window.APP_TIMEZONE_OFFSET = <?php echo $GLOBALS['config']['TIMEZONE_OFFSET']; ?>; // minutes from UTC
window.APP_TIMEZONE_ABBREV = '<?php echo $GLOBALS['config']['TIMEZONE_ABBREV']; ?>';
</script>
<script>
<script nonce="<?php echo $nonce; ?>">
// Store ticket data in a global variable (using json_encode for XSS safety)
window.ticketData = {
ticket_id: <?php echo json_encode($ticket['ticket_id']); ?>,
@@ -445,7 +446,7 @@ function formatDetails($details, $actionType) {
</div>
</div>
<!-- END OUTER FRAME -->
<script>
<script nonce="<?php echo $nonce; ?>">
// Initialize the ticket view
document.addEventListener('DOMContentLoaded', function() {
if (typeof showTab === 'function') {
@@ -455,7 +456,7 @@ function formatDetails($details, $actionType) {
}
});
</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);
@@ -597,6 +598,6 @@ function formatDetails($details, $actionType) {
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/settings.js"></script>
</body>
</html>

View File

@@ -1,6 +1,9 @@
<?php
// Admin view for managing API keys
// Receives $apiKeys from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -11,12 +14,9 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script>
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -161,7 +161,7 @@
</div>
</div>
<script>
<script nonce="<?php echo $nonce; ?>">
document.getElementById('generateKeyForm').addEventListener('submit', async function(e) {
e.preventDefault();

View File

@@ -1,6 +1,9 @@
<?php
// Admin view for managing custom fields
// Receives $customFields from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -11,11 +14,8 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script>
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -154,8 +154,8 @@
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Custom Field';
document.getElementById('fieldForm').reset();

View File

@@ -1,6 +1,9 @@
<?php
// Admin view for managing recurring tickets
// Receives $recurringTickets from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -11,11 +14,8 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script>
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -189,8 +189,8 @@
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
function showCreateModal() {
document.getElementById('modalTitle').textContent = 'Create Recurring Ticket';
document.getElementById('recurringForm').reset();

View File

@@ -1,6 +1,9 @@
<?php
// Admin view for managing ticket templates
// Receives $templates from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -11,11 +14,8 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script>
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -160,8 +160,8 @@
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
const templates = <?php echo json_encode($templates ?? []); ?>;
function showCreateModal() {

View File

@@ -1,6 +1,9 @@
<?php
// Admin view for workflow/status transitions designer
// Receives $workflows from controller
require_once __DIR__ . '/../../middleware/SecurityHeadersMiddleware.php';
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
$nonce = SecurityHeadersMiddleware::getNonce();
?>
<!DOCTYPE html>
<html lang="en">
@@ -11,11 +14,8 @@
<link rel="icon" type="image/png" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/images/favicon.png">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/dashboard.css">
<link rel="stylesheet" href="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/css/ticket.css">
<script>
window.CSRF_TOKEN = '<?php
require_once __DIR__ . '/../../middleware/CsrfMiddleware.php';
echo CsrfMiddleware::getToken();
?>';
<script nonce="<?php echo $nonce; ?>">
window.CSRF_TOKEN = '<?php echo CsrfMiddleware::getToken(); ?>';
</script>
</head>
<body>
@@ -177,8 +177,8 @@
</div>
</div>
<script src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script>
<script nonce="<?php echo $nonce; ?>" src="<?php echo $GLOBALS['config']['ASSETS_URL']; ?>/js/toast.js"></script>
<script nonce="<?php echo $nonce; ?>">
const workflows = <?php echo json_encode($workflows ?? []); ?>;
function showCreateModal() {