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:
@@ -55,6 +55,9 @@ foreach ($ticketIds as $ticketId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create database connection (needed for visibility check)
|
||||
require_once dirname(__DIR__) . '/models/TicketModel.php';
|
||||
|
||||
// Create database connection
|
||||
$conn = new mysqli(
|
||||
$GLOBALS['config']['DB_HOST'],
|
||||
@@ -69,6 +72,33 @@ if ($conn->connect_error) {
|
||||
}
|
||||
|
||||
$bulkOpsModel = new BulkOperationsModel($conn);
|
||||
$ticketModel = new TicketModel($conn);
|
||||
|
||||
// Verify user can access all tickets in the bulk operation
|
||||
// (Admins can access all, but this is defense-in-depth)
|
||||
$accessibleTicketIds = [];
|
||||
$inaccessibleCount = 0;
|
||||
$tickets = $ticketModel->getTicketsByIds($ticketIds);
|
||||
|
||||
foreach ($ticketIds as $ticketId) {
|
||||
$ticketId = trim($ticketId);
|
||||
$ticket = $tickets[$ticketId] ?? null;
|
||||
|
||||
if ($ticket && $ticketModel->canUserAccessTicket($ticket, $_SESSION['user'])) {
|
||||
$accessibleTicketIds[] = $ticketId;
|
||||
} else {
|
||||
$inaccessibleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($accessibleTicketIds)) {
|
||||
$conn->close();
|
||||
echo json_encode(['success' => false, 'error' => 'No accessible tickets in selection']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Use only accessible ticket IDs
|
||||
$ticketIds = $accessibleTicketIds;
|
||||
|
||||
// Create bulk operation record
|
||||
$operationId = $bulkOpsModel->createBulkOperation($operationType, $ticketIds, $_SESSION['user']['user_id'], $parameters);
|
||||
@@ -90,11 +120,16 @@ if (isset($result['error'])) {
|
||||
'error' => $result['error']
|
||||
]);
|
||||
} else {
|
||||
$message = "Bulk operation completed: {$result['processed']} succeeded, {$result['failed']} failed";
|
||||
if ($inaccessibleCount > 0) {
|
||||
$message .= " ($inaccessibleCount skipped - no access)";
|
||||
}
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'operation_id' => $operationId,
|
||||
'processed' => $result['processed'],
|
||||
'failed' => $result['failed'],
|
||||
'message' => "Bulk operation completed: {$result['processed']} succeeded, {$result['failed']} failed"
|
||||
'skipped' => $inaccessibleCount,
|
||||
'message' => $message
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -41,26 +41,42 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify the associated ticket exists (access control)
|
||||
// Verify the associated ticket exists and user has access
|
||||
$conn = new mysqli(
|
||||
$GLOBALS['config']['DB_HOST'],
|
||||
$GLOBALS['config']['DB_USER'],
|
||||
$GLOBALS['config']['DB_PASS'],
|
||||
$GLOBALS['config']['DB_NAME']
|
||||
);
|
||||
if (!$conn->connect_error) {
|
||||
$ticketModel = new TicketModel($conn);
|
||||
$ticket = $ticketModel->getTicketById($attachment['ticket_id']);
|
||||
$conn->close();
|
||||
|
||||
if (!$ticket) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Associated ticket not found']);
|
||||
exit;
|
||||
}
|
||||
if ($conn->connect_error) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ticketModel = new TicketModel($conn);
|
||||
$ticket = $ticketModel->getTicketById($attachment['ticket_id']);
|
||||
|
||||
if (!$ticket) {
|
||||
$conn->close();
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Associated ticket not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if user has access to this ticket based on visibility settings
|
||||
if (!$ticketModel->canUserAccessTicket($ticket, $_SESSION['user'])) {
|
||||
$conn->close();
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Access denied to this ticket']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
// Build file path
|
||||
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads';
|
||||
$filePath = $uploadDir . '/' . $attachment['ticket_id'] . '/' . $attachment['filename'];
|
||||
|
||||
@@ -151,6 +151,15 @@ try {
|
||||
if (is_array($visibilityGroups)) {
|
||||
$visibilityGroups = implode(',', array_map('trim', $visibilityGroups));
|
||||
}
|
||||
|
||||
// Validate internal visibility requires groups
|
||||
if ($data['visibility'] === 'internal' && (empty($visibilityGroups) || trim($visibilityGroups) === '')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Internal visibility requires at least one group to be specified'
|
||||
];
|
||||
}
|
||||
|
||||
$this->ticketModel->updateVisibility($id, $data['visibility'], $visibilityGroups, $this->userId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user