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:
+49
-6
@@ -38,12 +38,16 @@ 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
|
||||
// CSRF Protection for all state-changing methods (any non-GET/HEAD request)
|
||||
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'], true)) {
|
||||
$csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||
http_response_code(403);
|
||||
@@ -63,7 +67,11 @@ try {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$data) {
|
||||
throw new Exception("Invalid JSON data received");
|
||||
http_response_code(400);
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid JSON data received']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ticketId = isset($data['ticket_id']) ? trim((string)$data['ticket_id']) : '';
|
||||
@@ -75,6 +83,20 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Reject empty/whitespace-only comments
|
||||
$commentTextRaw = isset($data['comment_text']) ? trim((string)$data['comment_text']) : '';
|
||||
if ($commentTextRaw === '') {
|
||||
http_response_code(400);
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Comment text cannot be empty']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Never trust a client-supplied display name — always attribute the comment to
|
||||
// the authenticated session user.
|
||||
$data['user_name'] = $currentUser['display_name'] ?? $currentUser['username'] ?? 'User';
|
||||
|
||||
// Verify user can access the ticket before allowing a comment
|
||||
$ticketModel = new TicketModel($conn);
|
||||
$ticket = $ticketModel->getTicketById($ticketId);
|
||||
@@ -97,6 +119,18 @@ try {
|
||||
$commentModel = new CommentModel($conn);
|
||||
$auditLog = new AuditLogModel($conn);
|
||||
|
||||
// If replying, the parent comment must belong to this same (accessible) ticket.
|
||||
if (isset($data['parent_comment_id']) && $data['parent_comment_id'] !== null && $data['parent_comment_id'] !== '') {
|
||||
$parentComment = $commentModel->getCommentById((int)$data['parent_comment_id']);
|
||||
if (!$parentComment || (string)$parentComment['ticket_id'] !== (string)$ticketId) {
|
||||
http_response_code(400);
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid parent comment']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract @mentions from comment text
|
||||
$mentions = $commentModel->extractMentions($data['comment_text'] ?? '');
|
||||
$mentionedUsers = [];
|
||||
@@ -130,6 +164,7 @@ try {
|
||||
$authorDisplay = $currentUser['display_name'] ?? $currentUser['username'] ?? null;
|
||||
$commentText = $data['comment_text'] ?? '';
|
||||
$ticketTitle = $ticket['title'] ?? "Ticket #{$ticketId}";
|
||||
$ticketVisibility = $ticket['visibility'] ?? 'public';
|
||||
|
||||
// @mention notifications — resolve usernames → Matrix IDs via Synapse Admin API
|
||||
if (!empty($mentionedUsers)) {
|
||||
@@ -142,7 +177,14 @@ try {
|
||||
|
||||
// General comment notification (opt-in via MATRIX_NOTIFY_COMMENTS)
|
||||
if (!empty($GLOBALS['config']['MATRIX_NOTIFY_COMMENTS'])) {
|
||||
NotificationHelper::sendCommentNotification($ticketId, $ticketTitle, $commentText, $authorDisplay);
|
||||
NotificationHelper::sendCommentNotification(
|
||||
$ticketId,
|
||||
$ticketTitle,
|
||||
$commentText,
|
||||
$authorDisplay,
|
||||
$ticketVisibility !== 'public',
|
||||
$ticketVisibility
|
||||
);
|
||||
}
|
||||
|
||||
// Notify watchers of the new comment
|
||||
@@ -152,7 +194,8 @@ try {
|
||||
$ticketTitle,
|
||||
'comment_added',
|
||||
['author' => $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…')],
|
||||
(int)$userId
|
||||
(int)$userId,
|
||||
$ticketVisibility
|
||||
);
|
||||
|
||||
// Add mentioned users to result for frontend
|
||||
|
||||
Reference in New Issue
Block a user