Harden attachment deletion and template CRUD validation

- delete_attachment.php: add realpath() path traversal check before
  unlink() — mirrors the defense-in-depth already in download_attachment.php;
  also cast ticket_id to int when building the path
- manage_templates.php: add input validation to POST and PUT handlers:
  required field checks, max length caps (name 100, title 255, desc 64KB),
  allowlist validation for category/type, priority clamped to 1-5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 13:41:22 -04:00
parent e2c23d0405
commit 82aa4bf5de
2 changed files with 62 additions and 19 deletions
+10 -5
View File
@@ -80,12 +80,17 @@ try {
ResponseHelper::forbidden('You do not have permission to delete this attachment'); ResponseHelper::forbidden('You do not have permission to delete this attachment');
} }
// Delete the file // Delete the file — use realpath() to prevent path traversal
$uploadDir = $GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads'; $uploadDir = realpath($GLOBALS['config']['UPLOAD_DIR'] ?? dirname(__DIR__) . '/uploads');
$filePath = $uploadDir . '/' . $attachment['ticket_id'] . '/' . $attachment['filename']; $filePath = $uploadDir . '/' . (int)$attachment['ticket_id'] . '/' . $attachment['filename'];
$realPath = realpath($filePath);
if (file_exists($filePath)) { if ($realPath !== false) {
if (!unlink($filePath)) { // Ensure the resolved path is still inside the upload directory
if (strncmp($realPath, $uploadDir . DIRECTORY_SEPARATOR, strlen($uploadDir) + 1) !== 0) {
ResponseHelper::forbidden('Access denied');
}
if (!unlink($realPath)) {
ResponseHelper::serverError('Failed to delete file'); ResponseHelper::serverError('Failed to delete file');
} }
} }
+52 -14
View File
@@ -73,17 +73,36 @@ try {
case 'POST': case 'POST':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
// Validate required fields and lengths
$templateName = trim($data['template_name'] ?? '');
$titleTemplate = trim($data['title_template'] ?? '');
if (!$templateName || mb_strlen($templateName) > 100) {
echo json_encode(['success' => false, 'error' => 'Template name is required (max 100 chars)']);
exit;
}
if (!$titleTemplate || mb_strlen($titleTemplate) > 255) {
echo json_encode(['success' => false, 'error' => 'Title template is required (max 255 chars)']);
exit;
}
$allowedCategories = ['General','Hardware','Software','Network','Security'];
$allowedTypes = ['Issue','Maintenance','Install','Task','Upgrade','Problem'];
$category = in_array($data['category'] ?? '', $allowedCategories) ? $data['category'] : 'General';
$type = in_array($data['type'] ?? '', $allowedTypes) ? $data['type'] : 'Issue';
$priority = max(1, min(5, (int)($data['default_priority'] ?? 4)));
$isActive = $data['is_active'] ? 1 : 0;
$description = mb_substr($data['description_template'] ?? '', 0, 65535);
$stmt = $conn->prepare("INSERT INTO ticket_templates $stmt = $conn->prepare("INSERT INTO ticket_templates
(template_name, title_template, description_template, category, type, default_priority, is_active) (template_name, title_template, description_template, category, type, default_priority, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?)"); VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param('sssssii', $stmt->bind_param('sssssii',
$data['template_name'], $templateName,
$data['title_template'], $titleTemplate,
$data['description_template'], $description,
$data['category'], $category,
$data['type'], $type,
$data['default_priority'] ?? 4, $priority,
$data['is_active'] ?? 1 $isActive
); );
if ($stmt->execute()) { if ($stmt->execute()) {
@@ -103,18 +122,37 @@ try {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
// Validate required fields and lengths
$templateName = trim($data['template_name'] ?? '');
$titleTemplate = trim($data['title_template'] ?? '');
if (!$templateName || mb_strlen($templateName) > 100) {
echo json_encode(['success' => false, 'error' => 'Template name is required (max 100 chars)']);
exit;
}
if (!$titleTemplate || mb_strlen($titleTemplate) > 255) {
echo json_encode(['success' => false, 'error' => 'Title template is required (max 255 chars)']);
exit;
}
$allowedCategories = ['General','Hardware','Software','Network','Security'];
$allowedTypes = ['Issue','Maintenance','Install','Task','Upgrade','Problem'];
$category = in_array($data['category'] ?? '', $allowedCategories) ? $data['category'] : 'General';
$type = in_array($data['type'] ?? '', $allowedTypes) ? $data['type'] : 'Issue';
$priority = max(1, min(5, (int)($data['default_priority'] ?? 4)));
$isActive = $data['is_active'] ? 1 : 0;
$description = mb_substr($data['description_template'] ?? '', 0, 65535);
$stmt = $conn->prepare("UPDATE ticket_templates SET $stmt = $conn->prepare("UPDATE ticket_templates SET
template_name = ?, title_template = ?, description_template = ?, template_name = ?, title_template = ?, description_template = ?,
category = ?, type = ?, default_priority = ?, is_active = ? category = ?, type = ?, default_priority = ?, is_active = ?
WHERE template_id = ?"); WHERE template_id = ?");
$stmt->bind_param('sssssiii', $stmt->bind_param('sssssiii',
$data['template_name'], $templateName,
$data['title_template'], $titleTemplate,
$data['description_template'], $description,
$data['category'], $category,
$data['type'], $type,
$data['default_priority'] ?? 4, $priority,
$data['is_active'] ?? 1, $isActive,
$id $id
); );