Fix API security: dependency/visibility leaks, authz, CSRF, comment spoofing
- ticket_dependencies.php: pass current user id/groups/is_admin into the visibility-filtered DependencyModel methods; drop (int) casts that stripped leading zeros from varchar ticket_ids - update_ticket.php: authorize visibility changes (admin or creator only); enforce requires_comment transitions server-side (400 + requires_comment flag so the client can prompt-and-retry); return proper 401/400/403 - add_comment.php: take commenter name from the session not the client (anti-spoofing); validate parent_comment_id belongs to the ticket; reject empty comments; pass ticket visibility to notifications so non-public comment bodies aren't leaked - add_comment/update_comment/bulk_operation: validate CSRF for all state-changing methods, not just POST - bootstrap.php: return the current CSRF token on rejection and never rotate it on a rejected request, so a desynced client can auto-recover - correct auth->401 and validation->400 status codes across these endpoints Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+54
-6
@@ -34,7 +34,11 @@ try {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
|
||||
throw new Exception("Authentication required");
|
||||
ob_end_clean();
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Authentication required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// CSRF Protection
|
||||
@@ -115,7 +119,8 @@ try {
|
||||
if (empty($updateData['title'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Title cannot be empty'
|
||||
'error' => 'Title cannot be empty',
|
||||
'http_status' => 400
|
||||
];
|
||||
}
|
||||
|
||||
@@ -123,7 +128,8 @@ try {
|
||||
if ($updateData['priority'] < 1 || $updateData['priority'] > 5) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Priority must be between 1 and 5'
|
||||
'error' => 'Priority must be between 1 and 5',
|
||||
'http_status' => 400
|
||||
];
|
||||
}
|
||||
|
||||
@@ -137,11 +143,32 @@ try {
|
||||
$visibilityGroups = implode(',', array_map('trim', $visibilityGroups));
|
||||
}
|
||||
|
||||
// Authorization: only an admin or the ticket's creator may change
|
||||
// visibility. Enforce only when the requested visibility actually
|
||||
// differs so ordinary edits that re-send the same value aren't blocked.
|
||||
$currentVisibility = $currentTicket['visibility'] ?? 'public';
|
||||
$currentGroups = $currentTicket['visibility_groups'] ?? null;
|
||||
$groupsProvided = array_key_exists('visibility_groups', $data);
|
||||
$visibilityChanged = ($data['visibility'] !== $currentVisibility)
|
||||
|| ($groupsProvided && (string)$visibilityGroups !== (string)$currentGroups);
|
||||
if ($visibilityChanged) {
|
||||
$isCreator = $this->userId !== null
|
||||
&& (int)($currentTicket['created_by'] ?? 0) === (int)$this->userId;
|
||||
if (!$this->isAdmin && !$isCreator) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'You do not have permission to change ticket visibility',
|
||||
'http_status' => 403
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Internal visibility requires at least one group
|
||||
if ($data['visibility'] === 'internal' && (empty($visibilityGroups) || trim($visibilityGroups) === '')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Internal visibility requires at least one group to be specified'
|
||||
'error' => 'Internal visibility requires at least one group to be specified',
|
||||
'http_status' => 400
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -160,6 +187,19 @@ try {
|
||||
'error' => 'Status transition not allowed: ' . $currentTicket['status'] . ' → ' . $updateData['status']
|
||||
];
|
||||
}
|
||||
|
||||
// Enforce requires_comment transitions server-side.
|
||||
if ($this->workflowModel->transitionRequiresComment($currentTicket['status'], $updateData['status'])) {
|
||||
$comment = trim((string)($data['comment'] ?? $data['comment_text'] ?? ''));
|
||||
if ($comment === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'A comment is required for this status change',
|
||||
'requires_comment' => true,
|
||||
'http_status' => 400
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update ticket with user tracking and optional optimistic locking
|
||||
@@ -257,11 +297,19 @@ try {
|
||||
$data = json_decode($input, true);
|
||||
|
||||
if (!$data) {
|
||||
throw new Exception("Invalid JSON data received: " . $input);
|
||||
ob_end_clean();
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid JSON data received']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($data['ticket_id'])) {
|
||||
throw new Exception("Missing ticket_id parameter");
|
||||
ob_end_clean();
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Missing ticket_id parameter']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ticketId = trim((string)$data['ticket_id']);
|
||||
|
||||
Reference in New Issue
Block a user