From de5c781f5761a6860a94cbbee0d6521eee0bbae4 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:02:18 +0100 Subject: [PATCH 1/7] Deny direct HTTP access to attachments and require download via UI --- attachments/.htaccess | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/attachments/.htaccess b/attachments/.htaccess index 8d945254..bff6cbc7 100644 --- a/attachments/.htaccess +++ b/attachments/.htaccess @@ -1,8 +1,15 @@ -AddHandler cgi-script .php .php2 .php3 .php4 .php5 .php6 .php7 .php8 .php9 .pl .py .js .jsp .asp .htm .html .$ - Options -ExecCGI -Indexes -#grant access only if files with specific extensions are uploaded - - Require all granted - +# Deny all direct HTTP access to files stored in this directory. +# Attachments must be served through the application (AttachmentsUI) so that authentication and authorization checks are always enforced. + +# For Apache 2.4 and later (using mod_authz_core): deny all requests to this directory. + + Require all denied + + +# For older Apache versions or when mod_authz_core is not available: use the legacy access control syntax to deny all requests to this directory. + + Order deny,allow + Deny from all + From 0b489bb88368f6aadb373e5283fb365ae0e650e5 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:15:25 +0100 Subject: [PATCH 2/7] Add server-side whitelist for allowed attachment upload types --- lib/Attachments.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/Attachments.php b/lib/Attachments.php index 23b4e6bc..8f2bf7a1 100755 --- a/lib/Attachments.php +++ b/lib/Attachments.php @@ -955,6 +955,27 @@ public function createFromUpload($dataItemType, $dataItemID, $fileField, return false; } + /* Restrict uploads to a whitelist of allowed file extensions. + * This is a server-side validation which cannot be bypassed by + * manipulating client-side restrictions. + */ + $allowedExtensions = array( + 'bmp', 'csv', 'doc', 'docx', 'heic', + 'jpeg', 'jpg', 'msg', 'odg', 'odt', + 'pages', 'pdf', 'png', 'ppt', 'pptx', + 'rtf', 'tiff', 'wpd', 'wps', 'xls', + 'xlsx', 'xps' + ); + + $extension = FileUtility::getFileExtension($originalFilename); + + if (!in_array($extension, $allowedExtensions, true)) + { + $this->_isError = true; + $this->_error = 'This file type is not allowed for upload.'; + return false; + } + /* This usually indicates an error. */ if ($fileSize <= 0) { From 215e59c39857826e5eaf30694e2c3f94b784b964 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:25:49 +0100 Subject: [PATCH 3/7] Avoid static FileUtility access in attachment upload validation --- lib/Attachments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Attachments.php b/lib/Attachments.php index 8f2bf7a1..5c4f591b 100755 --- a/lib/Attachments.php +++ b/lib/Attachments.php @@ -967,7 +967,7 @@ public function createFromUpload($dataItemType, $dataItemID, $fileField, 'xlsx', 'xps' ); - $extension = FileUtility::getFileExtension($originalFilename); + $extension = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION)); if (!in_array($extension, $allowedExtensions, true)) { From 3220f8438dd6622f495ebc58fc42eaab41f0bc31 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:38:17 +0100 Subject: [PATCH 4/7] Revert "Avoid static FileUtility access in attachment upload validation" This reverts commit 215e59c39857826e5eaf30694e2c3f94b784b964. --- lib/Attachments.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Attachments.php b/lib/Attachments.php index 5c4f591b..8f2bf7a1 100755 --- a/lib/Attachments.php +++ b/lib/Attachments.php @@ -967,7 +967,7 @@ public function createFromUpload($dataItemType, $dataItemID, $fileField, 'xlsx', 'xps' ); - $extension = strtolower(pathinfo($originalFilename, PATHINFO_EXTENSION)); + $extension = FileUtility::getFileExtension($originalFilename); if (!in_array($extension, $allowedExtensions, true)) { From eccc6f28133aa9d64c8cc8c229df542006a21209 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:40:00 +0100 Subject: [PATCH 5/7] Update UI attachment links to avoid direct attachments/ access --- modules/candidates/CreateImageAttachmentModal.tpl | 4 ++-- modules/candidates/Show.tpl | 4 ++-- modules/settings/Backup.tpl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/candidates/CreateImageAttachmentModal.tpl b/modules/candidates/CreateImageAttachmentModal.tpl index 9f2deb32..39c4460b 100755 --- a/modules/candidates/CreateImageAttachmentModal.tpl +++ b/modules/candidates/CreateImageAttachmentModal.tpl @@ -9,8 +9,8 @@ attachmentsRS as $rowNumber => $attachmentsData): ?>
- - + +
diff --git a/modules/candidates/Show.tpl b/modules/candidates/Show.tpl index 92ca84f4..5513c5b6 100755 --- a/modules/candidates/Show.tpl +++ b/modules/candidates/Show.tpl @@ -237,8 +237,8 @@ use OpenCATS\UI\CandidateDuplicateQuickActionMenu; - - + + diff --git a/modules/settings/Backup.tpl b/modules/settings/Backup.tpl index 30935f1b..88f72b90 100755 --- a/modules/settings/Backup.tpl +++ b/modules/settings/Backup.tpl @@ -46,7 +46,7 @@ (_($attachmentsData['fileSize']) ?>)  - + _($attachmentsData['originalFilename']) ?> From 405782d04a1e279252552a54915dc19dc9d7b984 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:45:58 +0100 Subject: [PATCH 6/7] Remove legacy attachment download helper --- ajax/getAttachmentLocal.php | 91 ---------------------- js/attachment.js | 110 --------------------------- modules/candidates/Questionnaire.tpl | 2 +- modules/candidates/Show.tpl | 4 +- modules/companies/Show.tpl | 2 +- modules/contacts/Show.tpl | 2 +- modules/joborders/Show.tpl | 4 +- 7 files changed, 7 insertions(+), 208 deletions(-) delete mode 100755 ajax/getAttachmentLocal.php delete mode 100755 js/attachment.js diff --git a/ajax/getAttachmentLocal.php b/ajax/getAttachmentLocal.php deleted file mode 100755 index 5710b9a1..00000000 --- a/ajax/getAttachmentLocal.php +++ /dev/null @@ -1,91 +0,0 @@ -isRequiredIDValid('id')) -{ - $interface->outputXMLErrorPage(-2, 'No attachment ID specified.'); - die(); -} - -$attachmentID = $_POST['id']; - -$attachments = new Attachments(-1); - -$rs = $attachments->get($attachmentID, false); - -if (!isset($rs['directoryName']) || - !isset($rs['storedFilename']) || - md5($rs['directoryName']) != $_POST['directoryNameHash']) -{ - $interface->outputXMLErrorPage(-2, 'Invalid directory name hash.'); - die(); -} - -$directoryName = $rs['directoryName']; -$fileName = $rs['storedFilename']; - -/* Check for the existence of the backup. If it is gone, send the user to a page informing them to press back and generate the backup again. */ -if ($rs['contentType'] == 'catsbackup') -{ - if (!file_exists('attachments/'.$directoryName.'/'.$fileName)) - { - $interface->outputXMLErrorPage(-2, 'The specified backup file no longer exists. Please press back and regenerate the backup before downloading. We are sorry for the inconvenience.'); - die(); - } -} - -$url = 'attachments/'.$directoryName.'/'.$fileName; - -if (!eval(Hooks::get('ATTACHMENT_RETRIEVAL'))) return; - -if (!file_exists('attachments/'.$directoryName.'/'.$fileName)) -{ - $interface->outputXMLErrorPage(-2, 'The file is temporarily unavailable for download. Please try again.'); - die(); -} - -$output = - "\n" . - " 0\n" . - " \n" . - " 1\n" . - "\n"; - -/* Send back the XML data. */ -$interface->outputXMLPage($output); - -?> diff --git a/js/attachment.js b/js/attachment.js deleted file mode 100755 index 17aaf04d..00000000 --- a/js/attachment.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * CATS - * Attachment JavaScript Library - * - * Copyright (C) 2005 - 2007 Cognizo Technologies, Inc. - * - * - * The contents of this file are subject to the CATS Public License - * Version 1.1a (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://www.catsone.com/. - * - * Software distributed under the License is distributed on an "AS IS" - * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the - * License for the specific language governing rights and limitations - * under the License. - * - * The Original Code is "CATS Standard Edition". - * - * The Initial Developer of the Original Code is Cognizo Technologies, Inc. - * Portions created by the Initial Developer are Copyright (C) 2005 - 2007 - * (or from the year in which this file was created to the year 2007) by - * Cognizo Technologies, Inc. All Rights Reserved. - * - * - * $Id: attachment.js 3078 2007-09-21 20:25:28Z will $ - */ - -var _spanObject; -var downloadBlock = false; -var downloadBlockUrl = false; -var downloadCancel = false; - - -function doPrepareAndDownload(getVars, url, spanObject, sessionCookie) -{ - if (downloadBlock) - { - if (spanObject != _spanObject) - { - alert ('A file is already being downloaded, please wait...'); - } - return; - } - - downloadBlock = true; - downloadBlockUrl = url; - - spanObject.innerHTML = "
 Preparing Download...
"; - - _spanObject = spanObject; - - var http = AJAX_getXMLHttpObject(); - - /* Build HTTP POST data. */ - var POSTData = '&' + getVars; - - /* Anonymous callback function triggered when HTTP response is received. */ - var callBack = function () - { - if (http.readyState != 4) - { - return; - } - - //alert(http.responseText); - - if (!http.responseXML) - { - var errorMessage = "An error occurred while receiving a response from the server.\n\n" - + http.responseText; - alert(errorMessage); - downloadBlock = false; - return; - } - - /* Return if we have any errors. */ - var errorCodeNode = http.responseXML.getElementsByTagName('errorcode').item(0); - var errorMessageNode = http.responseXML.getElementsByTagName('errormessage').item(0); - if (!errorCodeNode.firstChild || errorCodeNode.firstChild.nodeValue != '0') - { - var errorMessage = "An error occurred while receiving a response from the server.\n\n" - + errorMessageNode.firstChild.nodeValue; - alert(errorMessage); - downloadBlock = false; - return; - } - - if (!downloadCancel) - { - window.location.href = url; - } - - downloadCancel = false; - - setTimeout('downloadBlock = false; if(typeof(_spanObject != "undefined") && typeof(_spanObject.innerHTML != "undefined")) _spanObject.innerHTML = \'\';', 500); - - } - - AJAX_callCATSFunction( - http, - 'getAttachmentLocal', - POSTData, - callBack, - 0, - sessionCookie, - false, - false - ); -} diff --git a/modules/candidates/Questionnaire.tpl b/modules/candidates/Questionnaire.tpl index 1c073102..ee4a8a65 100755 --- a/modules/candidates/Questionnaire.tpl +++ b/modules/candidates/Questionnaire.tpl @@ -1,5 +1,5 @@ -cData['firstName'].' '.$this->cData['lastName'] . ' Questionnaire', array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js', 'js/attachment.js')); ?> +cData['firstName'].' '.$this->cData['lastName'] . ' Questionnaire', array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js')); ?> print): ?> active); ?> diff --git a/modules/candidates/Show.tpl b/modules/candidates/Show.tpl index 5513c5b6..6e3286b1 100755 --- a/modules/candidates/Show.tpl +++ b/modules/candidates/Show.tpl @@ -4,9 +4,9 @@ use OpenCATS\UI\CandidateQuickActionMenu; use OpenCATS\UI\CandidateDuplicateQuickActionMenu; ?> isPopup): ?> - data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js', 'js/attachment.js', 'modules/candidates/quickAction-candidates.js')); ?> + data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js', 'modules/candidates/quickAction-candidates.js')); ?> - data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js', 'js/attachment.js', 'modules/candidates/quickAction-candidates.js', 'modules/candidates/quickAction-duplicates.js')); ?> + data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js', 'js/sorttable.js', 'js/match.js', 'js/lib.js', 'js/pipeline.js', 'modules/candidates/quickAction-candidates.js', 'modules/candidates/quickAction-duplicates.js')); ?> active); ?> diff --git a/modules/companies/Show.tpl b/modules/companies/Show.tpl index ff6100d6..052aa168 100755 --- a/modules/companies/Show.tpl +++ b/modules/companies/Show.tpl @@ -2,7 +2,7 @@ include_once('./vendor/autoload.php'); use OpenCATS\UI\QuickActionMenu; ?> -data['name'], array( 'js/sorttable.js', 'js/attachment.js')); ?> +data['name'], array( 'js/sorttable.js')); ?> active); ?>
diff --git a/modules/contacts/Show.tpl b/modules/contacts/Show.tpl index 603b3c35..1c61c489 100755 --- a/modules/contacts/Show.tpl +++ b/modules/contacts/Show.tpl @@ -3,7 +3,7 @@ include_once('./vendor/autoload.php'); use OpenCATS\UI\QuickActionMenu; ?> -data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js', 'js/attachment.js')); ?> +data['firstName'].' '.$this->data['lastName'], array( 'js/activity.js')); ?> active); ?>
diff --git a/modules/joborders/Show.tpl b/modules/joborders/Show.tpl index 3e566aab..b9ec1daa 100755 --- a/modules/joborders/Show.tpl +++ b/modules/joborders/Show.tpl @@ -3,9 +3,9 @@ include_once('./vendor/autoload.php'); use OpenCATS\UI\QuickActionMenu; ?> isPopup): ?> - data['title'], array('js/sorttable.js', 'js/match.js', 'js/pipeline.js', 'js/attachment.js')); ?> + data['title'], array('js/sorttable.js', 'js/match.js', 'js/pipeline.js')); ?> - data['title'], array( 'js/sorttable.js', 'js/match.js', 'js/pipeline.js', 'js/attachment.js')); ?> + data['title'], array( 'js/sorttable.js', 'js/match.js', 'js/pipeline.js')); ?> active); ?>
From fb9978f28c6e895228274210cf1cb5c71c5a8075 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:19:44 +0100 Subject: [PATCH 7/7] Fix: prevent upload whitelist bypass for filenames without extension --- lib/FileUtility.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/FileUtility.php b/lib/FileUtility.php index 7277a961..584210c0 100755 --- a/lib/FileUtility.php +++ b/lib/FileUtility.php @@ -327,7 +327,15 @@ public static function getFileWithoutExtension($filename, */ public static function getFileExtension($filename) { - return strtolower(substr($filename, strrpos($filename, '.') + 1)); + $lastDotPosition = strrpos($filename, '.'); + + // Treat dotless names and dotfiles as having no extension. + if ($lastDotPosition === false || $lastDotPosition === 0) + { + return ''; + } + + return strtolower(substr($filename, $lastDotPosition + 1)); } /**