diff --git a/CRM/Contact/BAO/GroupContact.php b/CRM/Contact/BAO/GroupContact.php index 339af539ef..02dbe2c61b 100644 --- a/CRM/Contact/BAO/GroupContact.php +++ b/CRM/Contact/BAO/GroupContact.php @@ -694,6 +694,11 @@ public static function bulkAddContactsToGroup( $tracking = NULL ) { + $contactIDs = array_map('intval', $contactIDs); + $groupID = (int) $groupID; + $method = CRM_Core_DAO::escapeString($method); + $status = CRM_Core_DAO::escapeString($status); + $tracking = CRM_Core_DAO::escapeString($tracking ?: ''); $numContactsAdded = 0; $numContactsNotAdded = 0; diff --git a/CRM/Core/BAO/File.php b/CRM/Core/BAO/File.php index fa55f41feb..074fe3f225 100644 --- a/CRM/Core/BAO/File.php +++ b/CRM/Core/BAO/File.php @@ -161,6 +161,14 @@ public static function filePostProcess( $config = &CRM_Core_Config::singleton(); + // Security: validate file path is within CiviCRM upload directory (security/core#173) + $realData = realpath($data); + $uploadDir = realpath($config->uploadDir); + if (!$realData || !$uploadDir || strpos($realData, $uploadDir . DIRECTORY_SEPARATOR) !== 0) { + CRM_Core_Error::fatal(ts('Security: File path is not within the allowed upload directory')); + return; + } + $path = explode('/', $data); $filename = $path[count($path) - 1]; diff --git a/CRM/Core/Page/File.php b/CRM/Core/Page/File.php index d0e429b5f4..d0cb785fce 100644 --- a/CRM/Core/Page/File.php +++ b/CRM/Core/Page/File.php @@ -67,6 +67,12 @@ public function run() { CRM_Core_Error::fatal('Could not retrieve the file'); } + if (empty($downloadName) && substr($path, -8) === '.unknown') { + $mimeType = 'application/octet-stream'; + header('Content-Security-Policy: default-src \'none\''); + header('X-Content-Type-Options: nosniff'); + } + $buffer = file_get_contents($path); if ($buffer === FALSE) { CRM_Core_Error::fatal('The file is either empty or you do not have permission to retrieve the file'); diff --git a/api/v3/Extension.php b/api/v3/Extension.php deleted file mode 100644 index 59f2afd46c..0000000000 --- a/api/v3/Extension.php +++ /dev/null @@ -1,283 +0,0 @@ -getManager()->install($keys); - } - catch (CRM_Extension_Exception $e) { - return civicrm_api3_create_error($e->getMessage()); - } - - return civicrm_api3_create_success(); -} - -/** - * Enable an extension - * - * @param array $params input parameters - * - key: string, eg "com.example.myextension" - * - keys: mixed; array of string, eg array("com.example.myextension1", "com.example.myextension2") or string with comma-delimited list - * using 'keys' should be more performant than making multiple API calls with 'key' - * - * @return array API result - * @static void - * @access public - * @example ExtensionEnable.php - * - */ -function civicrm_api3_extension_enable($params) { - $keys = _civicrm_api3_getKeys($params); - if (count($keys) == 0) { - return civicrm_api3_create_success(); - } - - CRM_Extension_System::singleton()->getManager()->enable($keys); - return civicrm_api3_create_success(); -} - -/** - * Disable an extension - * - * @param array $params input parameters - * - key: string, eg "com.example.myextension" - * - keys: mixed; array of string, eg array("com.example.myextension1", "com.example.myextension2") or string with comma-delimited list - * using 'keys' should be more performant than making multiple API calls with 'key' - * - * @return array API result - * @static void - * @access public - * @example ExtensionDisable.php - * - */ -function civicrm_api3_extension_disable($params) { - $keys = _civicrm_api3_getKeys($params); - if (count($keys) == 0) { - return civicrm_api3_create_success(); - } - - CRM_Extension_System::singleton()->getManager()->disable($keys); - return civicrm_api3_create_success(); -} - -/** - * Uninstall an extension - * - * @param array $params input parameters - * - key: string, eg "com.example.myextension" - * - keys: array of string, eg array("com.example.myextension1", "com.example.myextension2") - * using 'keys' should be more performant than making multiple API calls with 'key' - * - removeFiles: bool, whether to remove source tree; default: FALSE - * - * @return array API result - * @static void - * @access public - * @example ExtensionUninstall.php - * - */ -function civicrm_api3_extension_uninstall($params) { - $keys = _civicrm_api3_getKeys($params); - if (count($keys) == 0) { - return civicrm_api3_create_success(); - } - - // TODO // $removeFiles = CRM_Utils_Array::value('removeFiles', $params, FALSE); - CRM_Extension_System::singleton()->getManager()->uninstall($keys); - return civicrm_api3_create_success(); -} - -/** - * Download and install an extension - * - * @param array $params input parameters - * - key: string, eg "com.example.myextension" - * - url: string eg "http://repo.com/myextension-1.0.zip" - * - * @return array API result - * @static void - * @access public - * @example ExtensionDownload.php - * - */ -function civicrm_api3_extension_download($params) { - if (!CRM_Utils_Array::arrayKeyExists('key', $params)) { - throw new API_Exception('Missing required parameter: key'); - } - - if (!CRM_Utils_Array::arrayKeyExists('url', $params)) { - if (!CRM_Extension_System::singleton()->getBrowser()->isEnabled()) { - throw new API_Exception('Automatic downloading is diabled. Try adding parameter "url"'); - } - if ($reqs = CRM_Extension_System::singleton()->getBrowser()->checkRequirements()) { - $first = array_shift($reqs); - throw new API_Exception($first['message']); - } - if ($info = CRM_Extension_System::singleton()->getBrowser()->getExtension($params['key'])) { - if ($info->downloadUrl) { - $params['url'] = $info->downloadUrl; - } - } - } - - if (!CRM_Utils_Array::arrayKeyExists('url', $params)) { - throw new API_Exception('Cannot resolve download url for extension. Try adding parameter "url"'); - } - - foreach (CRM_Extension_System::singleton()->getDownloader()->checkRequirements() as $requirement) { - return civicrm_api3_create_error($requirement['message']); - } - - if (!CRM_Extension_System::singleton()->getDownloader()->download($params['key'], $params['url'])) { - return civicrm_api3_create_error('Download failed - ZIP file is unavailable or malformed'); - } - CRM_Extension_System::singleton()->getCache()->flush(); - CRM_Extension_System::singleton(TRUE); - CRM_Extension_System::singleton()->getManager()->install([$params['key']]); - - return civicrm_api3_create_success(); -} - -/** - * Download and install an extension - * - * @param array $params input parameters - * - local: bool, whether to rescan local filesystem (default: TRUE) - * - remote: bool, whether to rescan remote repository (default: TRUE) - * - * @return array API result - * @static void - * @access public - * @example ExtensionRefresh.php - * - */ -function civicrm_api3_extension_refresh($params) { - $defaults = ['local' => TRUE, 'remote' => TRUE]; - $params = array_merge($defaults, $params); - - $system = CRM_Extension_System::singleton(TRUE); - - if ($params['local']) { - $system->getManager()->refresh(); - $system->getManager()->getStatuses(); // force immediate scan - } - - if ($params['remote']) { - if ($system->getBrowser()->isEnabled() && empty($system->getBrowser()->checkRequirements)) { - $system->getBrowser()->refresh(); - $system->getBrowser()->getExtensions(); // force immediate download - } - } - - return civicrm_api3_create_success(); -} - -/** - * Get a list of available extensions - * - * @return array API result - * @static void - * @access public - * @example ExtensionGet.php - * - */ -function civicrm_api3_extension_get($params) { - $statuses = CRM_Extension_System::singleton()->getManager()->getStatuses(); - $mapper = CRM_Extension_System::singleton()->getMapper(); - $result = []; - foreach ($statuses as $key => $status) { - //try { - // $info = (array) $mapper->keyToInfo($key); - //} catch (CRM_Extension_Exception $e) { - $info = []; - $info['key'] = $key; - //} - $info['status'] = $status; - $result[] = $info; - } - return civicrm_api3_create_success($result); -} - -/** - * Determine the list of extension keys - * - * @param array $params API request params with 'key' or 'keys' - * @return array of extension keys - * @throws API_Exception - */ -function _civicrm_api3_getKeys($params) { - if (CRM_Utils_Array::arrayKeyExists('keys', $params) && is_array($params['keys'])) { - return $params['keys']; - } - elseif (CRM_Utils_Array::arrayKeyExists('keys', $params) && is_string($params['keys'])) { - if ($params['keys'] == '') { - return []; - } - else { - return explode(API_V3_EXTENSION_DELIMITER, $params['keys']); - } - } - elseif (CRM_Utils_Array::arrayKeyExists('key', $params)) { - return [$params['key']]; - } - else { - throw new API_Exception('Missing required parameter: key or keys'); - } -} diff --git a/api/v3/File.php b/api/v3/File.php index 613dbdcb38..1086e4fa43 100644 --- a/api/v3/File.php +++ b/api/v3/File.php @@ -61,6 +61,13 @@ function civicrm_api3_file_create($params) { civicrm_api3_verify_mandatory($params, 'CRM_Core_DAO_File', ['uri']); + // Security: Validate URI to prevent path traversal attacks + if (isset($params['uri']) && $params['uri'] !== basename($params['uri'])) { + throw new CRM_Core_Exception( + 'Invalid URI: must not contain directory separators or path traversal sequences' + ); + } + if (!isset($params['upload_date'])) { $params['upload_date'] = date("Ymd"); } diff --git a/templates/CRM/Admin/Page/EventTemplate.tpl b/templates/CRM/Admin/Page/EventTemplate.tpl index c6979bb922..2ca4da3ef6 100644 --- a/templates/CRM/Admin/Page/EventTemplate.tpl +++ b/templates/CRM/Admin/Page/EventTemplate.tpl @@ -55,9 +55,9 @@ {foreach from=$rows item=row} - {$row.template_title} - {$row.event_type} - {$row.participant_role} + {$row.template_title|escape} + {$row.event_type|escape} + {$row.participant_role|escape} {$row.participant_listing} {if $row.is_public eq 1}{ts}Yes{/ts} {else} {ts}No{/ts} {/if} {if $row.is_monetary eq 1}{ts}Yes{/ts} {else} {ts}No{/ts} {/if} diff --git a/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl b/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl index 7f75828d86..ee3ea45571 100644 --- a/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl +++ b/templates/CRM/Contact/Page/View/CustomDataFieldView.tpl @@ -38,7 +38,7 @@ {if $element.field_value.image} {include file='CRM/common/modal.tpl' modalImage=$element.field_value.image} {else} - {$element.field_value.fileName} + {$element.field_value.fileName|escape} {/if} {elseif $element.field_data_type EQ 'ContactReference' && $element.contact_ref_id} {*Contact ref id passed if user has sufficient permissions - so make a link.*} diff --git a/templates/CRM/Contact/Page/View/Summary.tpl b/templates/CRM/Contact/Page/View/Summary.tpl index 92456b9cf9..1c1848e815 100644 --- a/templates/CRM/Contact/Page/View/Summary.tpl +++ b/templates/CRM/Contact/Page/View/Summary.tpl @@ -204,8 +204,8 @@ {foreach from=$website item=item} {if $item.url} - {$item.website_type} {ts}Website{/ts} - {$item.url} + {$item.website_type|escape} {ts}Website{/ts} + {$item.url|escape} {/if} diff --git a/templates/CRM/Custom/Page/CustomDataView.tpl b/templates/CRM/Custom/Page/CustomDataView.tpl index 294384290e..f9a9e41609 100644 --- a/templates/CRM/Custom/Page/CustomDataView.tpl +++ b/templates/CRM/Custom/Page/CustomDataView.tpl @@ -72,7 +72,7 @@ {if $element.field_value.image} {include file='CRM/common/modal.tpl' modalImage=$element.field_value.image} {else} - {$element.field_value.fileName} + {$element.field_value.fileName|escape} {/if} {else} {$element.field_value|escape} diff --git a/templates/CRM/Event/Form/Selector.tpl b/templates/CRM/Event/Form/Selector.tpl index 023ce0bca2..0484f52b6f 100644 --- a/templates/CRM/Event/Form/Selector.tpl +++ b/templates/CRM/Event/Form/Selector.tpl @@ -110,7 +110,7 @@ {$row.participant_register_date|crmDate} {$row.participant_status} - {$row.participant_role_id} + {$row.participant_role_id|escape} {$row.action|replace:'xx':$participant_id} {/foreach} diff --git a/templates/CRM/Event/Page/ManageEvent.tpl b/templates/CRM/Event/Page/ManageEvent.tpl index 41609e3288..cd00685ad7 100644 --- a/templates/CRM/Event/Page/ManageEvent.tpl +++ b/templates/CRM/Event/Page/ManageEvent.tpl @@ -61,7 +61,7 @@ {foreach from=$rows item=row} {$row.id} - {$row.event_type} + {$row.event_type|escape} {$row.title|smarty:nodefaults|purify}