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

@@ -307,14 +307,45 @@ switch (true) {
'to' => $_GET['date_to'] ?? date('Y-m-d')
];
// Optimized query using LEFT JOINs with aggregated subqueries instead of correlated subqueries
// This eliminates N+1 query pattern and runs much faster with many users
$sql = "SELECT
u.user_id, u.username, u.display_name, u.is_admin,
(SELECT COUNT(*) FROM tickets t WHERE t.created_by = u.user_id AND DATE(t.created_at) BETWEEN ? AND ?) as tickets_created,
(SELECT COUNT(*) FROM tickets t WHERE t.assigned_to = u.user_id AND t.status = 'Closed' AND DATE(t.updated_at) BETWEEN ? AND ?) as tickets_resolved,
(SELECT COUNT(*) FROM ticket_comments tc WHERE tc.user_id = u.user_id AND DATE(tc.created_at) BETWEEN ? AND ?) as comments_added,
(SELECT COUNT(*) FROM tickets t WHERE t.assigned_to = u.user_id AND DATE(t.created_at) BETWEEN ? AND ?) as tickets_assigned,
(SELECT MAX(al.created_at) FROM audit_log al WHERE al.user_id = u.user_id) as last_activity
COALESCE(tc.tickets_created, 0) as tickets_created,
COALESCE(tr.tickets_resolved, 0) as tickets_resolved,
COALESCE(cm.comments_added, 0) as comments_added,
COALESCE(ta.tickets_assigned, 0) as tickets_assigned,
al.last_activity
FROM users u
LEFT JOIN (
SELECT created_by, COUNT(*) as tickets_created
FROM tickets
WHERE DATE(created_at) BETWEEN ? AND ?
GROUP BY created_by
) tc ON u.user_id = tc.created_by
LEFT JOIN (
SELECT assigned_to, COUNT(*) as tickets_resolved
FROM tickets
WHERE status = 'Closed' AND DATE(updated_at) BETWEEN ? AND ?
GROUP BY assigned_to
) tr ON u.user_id = tr.assigned_to
LEFT JOIN (
SELECT user_id, COUNT(*) as comments_added
FROM ticket_comments
WHERE DATE(created_at) BETWEEN ? AND ?
GROUP BY user_id
) cm ON u.user_id = cm.user_id
LEFT JOIN (
SELECT assigned_to, COUNT(*) as tickets_assigned
FROM tickets
WHERE DATE(created_at) BETWEEN ? AND ?
GROUP BY assigned_to
) ta ON u.user_id = ta.assigned_to
LEFT JOIN (
SELECT user_id, MAX(created_at) as last_activity
FROM audit_log
GROUP BY user_id
) al ON u.user_id = al.user_id
ORDER BY tickets_created DESC, tickets_resolved DESC";
$stmt = $conn->prepare($sql);