From 338bed7eb7e6e1ad1aec7f1f10461e7d0dd0ccc4 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 8 Sep 2026 14:14:00 -0400 Subject: [PATCH] Add per-ticket attachment count/storage quota (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api/upload_attachment.php enforced a per-file size cap but nothing bounded the total number of attachments on a single ticket or their cumulative size over time — an authenticated low-privilege user could slowly fill the uploads/ disk by attaching many files across tickets, bounded only by the general rate limiter (which throttles request rate, not storage volume). Added MAX_ATTACHMENTS_PER_TICKET (50) and MAX_TOTAL_ATTACHMENT_SIZE_PER_TICKET (100MB) config defaults, enforced before move_uploaded_file() using AttachmentModel::getAttachmentCount() and getTotalSizeForTicket() — both already existed in the model with zero callers, apparently added for exactly this purpose but never wired in. Verified against a local MariaDB instance: with 3 existing 1MB attachments and a 3-attachment cap, the count check correctly rejects a 4th; with a 5MB total cap, a 2.5MB upload that would push the ticket over the limit is correctly rejected while a small one that fits is not. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X --- api/upload_attachment.php | 18 +++++++++++++++++- config/config.php | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/api/upload_attachment.php b/api/upload_attachment.php index 6fa4e48..481d99f 100644 --- a/api/upload_attachment.php +++ b/api/upload_attachment.php @@ -194,6 +194,23 @@ if ($file['size'] > $maxSize) { ResponseHelper::error('File size exceeds maximum allowed (' . AttachmentModel::formatFileSize($maxSize) . ')'); } +// Check per-ticket attachment count/storage quota — bounds an authenticated +// low-privilege user slowly filling the uploads/ disk across many tickets, +// which was previously bounded only by the request-rate limiter, not volume. +$attachmentModel = new AttachmentModel($conn); +$maxAttachments = $GLOBALS['config']['MAX_ATTACHMENTS_PER_TICKET'] ?? 50; +if ($attachmentModel->getAttachmentCount($ticketId) >= $maxAttachments) { + ResponseHelper::error("This ticket already has the maximum of {$maxAttachments} attachments"); +} + +$maxTotalSize = $GLOBALS['config']['MAX_TOTAL_ATTACHMENT_SIZE_PER_TICKET'] ?? 104857600; +if ($attachmentModel->getTotalSizeForTicket($ticketId) + $file['size'] > $maxTotalSize) { + ResponseHelper::error( + 'This upload would exceed the ticket\'s total attachment size limit of ' + . AttachmentModel::formatFileSize($maxTotalSize) + ); +} + // Get MIME type $finfo = new finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->file($file['tmp_name']); @@ -265,7 +282,6 @@ if (empty($originalFilename)) { // Save to database try { - $attachmentModel = new AttachmentModel($conn); $attachmentId = $attachmentModel->addAttachment( $ticketId, $uniqueFilename, diff --git a/config/config.php b/config/config.php index 3ffb242..80ab44e 100644 --- a/config/config.php +++ b/config/config.php @@ -115,6 +115,8 @@ $GLOBALS['config'] = [ // File upload settings 'MAX_UPLOAD_SIZE' => 10485760, // 10MB in bytes + 'MAX_ATTACHMENTS_PER_TICKET' => 50, + 'MAX_TOTAL_ATTACHMENT_SIZE_PER_TICKET' => 104857600, // 100MB in bytes 'ALLOWED_FILE_TYPES' => [ 'image/jpeg', 'image/png',