3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Utils_Check_Component_Security
extends CRM_Utils_Check_Component
{
20 * CMS have a different pattern to their default file path and URL.
22 * @todo Use Civi::paths instead?
25 public function getFilePathMarker() {
26 $config = CRM_Core_Config
::singleton();
27 switch ($config->userFramework
) {
37 * Check if our logfile is directly accessible.
39 * Per CiviCRM default the logfile sits in a folder which is
40 * web-accessible, and is protected by a default .htaccess
41 * configuration. If server config causes the .htaccess not to
42 * function as intended, there may be information disclosure.
44 * The debug log may be jam-packed with sensitive data, we don't
47 * Being able to be retrieved directly doesn't mean the logfile
48 * is browseable or visible to search engines; it means it can be
51 * @return CRM_Utils_Check_Message[]
54 public function checkLogFileIsNotAccessible() {
57 $config = CRM_Core_Config
::singleton();
59 $log = CRM_Core_Error
::createDebugLogger();
60 $log_filename = str_replace('\\', '/', $log->_filename
);
62 $filePathMarker = $this->getFilePathMarker();
64 // Hazard a guess at the URL of the logfile, based on common
66 if ($upload_url = explode($filePathMarker, $config->imageUploadURL
)) {
67 $url[] = $upload_url[0];
68 if ($log_path = explode($filePathMarker, $log_filename)) {
69 // CRM-17149: check if debug log path includes $filePathMarker
70 if (count($log_path) > 1) {
71 $url[] = $log_path[1];
72 $log_url = implode($filePathMarker, $url);
73 if ($this->fileExists($log_url)) {
74 $docs_url = $this->createDocUrl('the-log-file-should-not-be-accessible');
75 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
77 '<a href="%2">Read more about this warning</a>';
78 $messages[] = new CRM_Utils_Check_Message(
80 ts($msg, [1 => $log_url, 2 => $docs_url]),
81 ts('Security Warning'),
82 \Psr\Log\LogLevel
::WARNING
,
94 * Check if our uploads directory has accessible files.
96 * We'll test a handful of files randomly. Hazard a guess at the URL
97 * of the uploads dir, based on common CiviCRM layouts. Try and
98 * request the files, and if any are successfully retrieved, warn.
100 * Being retrievable doesn't mean the files are browseable or visible
101 * to search engines; it only means they can be requested directly.
103 * @return CRM_Utils_Check_Message[]
106 * @todo Test with WordPress, Joomla.
108 public function checkUploadsAreNotAccessible() {
111 $config = CRM_Core_Config
::singleton();
114 $config->customFileUploadDir
,
117 foreach ($privateDirs as $privateDir) {
118 $heuristicUrl = $this->guessUrl($privateDir);
119 if ($this->isDirAccessible($privateDir, $heuristicUrl)) {
120 $messages[] = new CRM_Utils_Check_Message(
122 ts('Files in the data directory (<a href="%3">%2</a>) should not be downloadable.'
124 . '<a href="%1">Read more about this warning</a>',
126 1 => $this->createDocUrl('uploads-should-not-be-accessible'),
130 ts('Private Files Readable'),
131 \Psr\Log\LogLevel
::WARNING
,
141 * Check if our uploads or ConfigAndLog directories have browseable
144 * Retrieve a listing of files from the local filesystem, and the
145 * corresponding path via HTTP. Then check and see if the local
146 * files are represented in the HTTP result; if so then warn. This
147 * MAY trigger false positives (if you have files named 'a', 'e'
148 * we'll probably match that).
150 * @return CRM_Utils_Check_Message[]
153 * @todo Test with WordPress, Joomla.
155 public function checkDirectoriesAreNotBrowseable() {
157 $config = CRM_Core_Config
::singleton();
159 $config->imageUploadDir
=> $config->imageUploadURL
,
162 // Setup index.html files to prevent browsing
163 foreach ($publicDirs as $publicDir => $publicUrl) {
164 CRM_Utils_File
::restrictBrowsing($publicDir);
167 // Test that $publicDir is not browsable
168 foreach ($publicDirs as $publicDir => $publicUrl) {
169 if ($this->isBrowsable($publicDir, $publicUrl)) {
170 $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.'
172 '<a href="%3">Read more about this warning</a>';
173 $docs_url = $this->createDocUrl('directories-should-not-be-browsable');
174 $messages[] = new CRM_Utils_Check_Message(
176 ts($msg, [1 => $publicDir, 2 => $publicDir, 3 => $docs_url]),
177 ts('Browseable Directories'),
178 \Psr\Log\LogLevel
::ERROR
,
188 * Check that some files are not present.
190 * These files have generally been deleted but Civi source tree but could be
191 * left online if one does a faulty upgrade.
193 * @return CRM_Utils_Check_Message[]
195 public function checkFilesAreNotPresent() {
196 $packages_path = rtrim(\Civi
::paths()->getPath('[civicrm.packages]/'), '/' . DIRECTORY_SEPARATOR
);
197 $vendor_path = rtrim(\Civi
::paths()->getPath('[civicrm.vendor]/'), '/' . DIRECTORY_SEPARATOR
);
202 // CRM-16005, upgraded from Civi <= 4.5.6
203 "{$packages_path}/dompdf/dompdf.php",
204 \Psr\Log\LogLevel
::CRITICAL
,
207 // CRM-16005, Civi >= 4.5.7
208 "{$packages_path}/vendor/dompdf/dompdf/dompdf.php",
209 \Psr\Log\LogLevel
::CRITICAL
,
212 // CRM-16005, Civi >= 4.6.0
213 "{$vendor_path}/dompdf/dompdf/dompdf.php",
214 \Psr\Log\LogLevel
::CRITICAL
,
218 "{$packages_path}/OpenFlashChart/php-ofc-library/ofc_upload_image.php",
219 \Psr\Log\LogLevel
::CRITICAL
,
222 "{$packages_path}/html2text/class.html2text.inc",
223 \Psr\Log\LogLevel
::CRITICAL
,
226 foreach ($files as $file) {
227 if (file_exists($file[0])) {
228 $messages[] = new CRM_Utils_Check_Message(
230 ts('File \'%1\' presents a security risk and should be deleted.', [1 => $file[0]]),
241 * Discourage use of remote profile forms.
242 * @return CRM_Utils_Check_Message[]
244 public function checkRemoteProfile() {
247 if (Civi
::settings()->get('remote_profile_submissions')) {
248 $messages[] = new CRM_Utils_Check_Message(
250 ts('Warning: External profile support (aka "HTML Snippet" support) is enabled in <a href="%1">system settings</a>. This setting may be prone to abuse. If you must retain it, consider HTTP throttling or other protections.',
251 [1 => CRM_Utils_System
::url('civicrm/admin/setting/misc', 'reset=1')]
253 ts('Remote Profiles Enabled'),
254 \Psr\Log\LogLevel
::WARNING
,
263 * Check that the sysadmin has not modified the Cxn security setup.
264 * @return CRM_Utils_Check_Message[]
266 public function checkCxnOverrides() {
268 if (defined('CIVICRM_CXN_CA') && CIVICRM_CXN_CA
!== 'CiviRootCA') {
269 $list[] = 'CIVICRM_CXN_CA';
271 if (defined('CIVICRM_CXN_APPS_URL') && CIVICRM_CXN_APPS_URL
!== \Civi\Cxn\Rpc\Constants
::OFFICIAL_APPMETAS_URL
) {
272 $list[] = 'CIVICRM_CXN_APPS_URL';
278 $messages[] = new CRM_Utils_Check_Message(
280 ts('The system administrator has disabled security settings (%1). Connections to remote applications are insecure.', [
281 1 => implode(', ', $list),
283 ts('Security Warning'),
284 \Psr\Log\LogLevel
::WARNING
,
293 * Determine whether $url is a public, browsable listing for $dir
301 public function isBrowsable($dir, $url) {
302 if (empty($dir) ||
empty($url) ||
!is_dir($dir)) {
308 // this could be a new system with no uploads (yet) -- so we'll make a file
309 $file = CRM_Utils_File
::createFakeFile($dir);
311 if ($file === FALSE) {
312 // Couldn't write the file
316 $content = @file_get_contents
("$url");
317 if (stristr($content, $file)) {
320 unlink("$dir/$file");
326 * Determine whether $url is a public version of $dir in which files
327 * are remotely accessible.
335 public function isDirAccessible($dir, $url) {
336 $url = rtrim($url, '/');
337 if (empty($dir) ||
empty($url) ||
!is_dir($dir)) {
342 $file = CRM_Utils_File
::createFakeFile($dir, 'delete me');
344 if ($file === FALSE) {
345 // Couldn't write the file
349 if ($this->fileExists("$url/$file")) {
350 $content = @file_get_contents
("$url/$file");
351 if (preg_match('/delete me/', $content)) {
356 unlink("$dir/$file");
366 public function createDocUrl($topic) {
367 return CRM_Utils_System
::docURL2('sysadmin/setup/security#' . $topic, TRUE);
371 * Make a guess about the URL that corresponds to $targetDir.
373 * @param string $targetDir
374 * Local path to a directory.
376 * a guessed URL for $realDir
378 public function guessUrl($targetDir) {
379 $filePathMarker = $this->getFilePathMarker();
380 $config = CRM_Core_Config
::singleton();
382 list($heuristicBaseUrl) = explode($filePathMarker, $config->imageUploadURL
);
383 list(, $heuristicSuffix) = array_pad(explode($filePathMarker, str_replace('\\', '/', $targetDir)), 2, '');
384 return $heuristicBaseUrl . $filePathMarker . $heuristicSuffix;