Merge pull request #19321 from colemanw/profileGetFieldsFix
[civicrm-core.git] / CRM / Utils / Check / Component / Security.php
CommitLineData
349b394e
CB
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
349b394e 5 | |
bc77d7c0
TO
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 |
349b394e 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
349b394e
CB
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
349b394e 16 */
3a0d0bbd 17class CRM_Utils_Check_Component_Security extends CRM_Utils_Check_Component {
349b394e 18
5c58b447
CB
19 /**
20 * CMS have a different pattern to their default file path and URL.
21 *
3a0d0bbd 22 * @todo Use Civi::paths instead?
02b3ba91 23 * @return string
5c58b447 24 */
a996bf82 25 public function getFilePathMarker() {
5c58b447
CB
26 $config = CRM_Core_Config::singleton();
27 switch ($config->userFramework) {
28 case 'Joomla':
29 return '/media/';
e7292422 30
5c58b447
CB
31 default:
32 return '/files/';
33 }
34 }
35
349b394e
CB
36 /**
37 * Check if our logfile is directly accessible.
38 *
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.
43 *
44 * The debug log may be jam-packed with sensitive data, we don't
45 * want that.
46 *
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
49 * requested directly.
50 *
02b3ba91 51 * @return CRM_Utils_Check_Message[]
349b394e
CB
52 * @see CRM-14091
53 */
23d89616 54 public function checkLogFileIsNotAccessible() {
be2fb01f 55 $messages = [];
e7d3e318 56
349b394e
CB
57 $config = CRM_Core_Config::singleton();
58
59 $log = CRM_Core_Error::createDebugLogger();
b35868f5 60 $log_filename = str_replace('\\', '/', $log->_filename);
349b394e 61
a996bf82 62 $filePathMarker = $this->getFilePathMarker();
5c58b447 63
349b394e
CB
64 // Hazard a guess at the URL of the logfile, based on common
65 // CiviCRM layouts.
5c58b447
CB
66 if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) {
67 $url[] = $upload_url[0];
68 if ($log_path = explode($filePathMarker, $log_filename)) {
4de448d7 69 // CRM-17149: check if debug log path includes $filePathMarker
7332c9bd 70 if (count($log_path) > 1) {
4de448d7
NG
71 $url[] = $log_path[1];
72 $log_url = implode($filePathMarker, $url);
4094935a 73 if ($this->fileExists($log_url)) {
4aae9070 74 $docs_url = $this->createDocUrl('the-log-file-should-not-be-accessible');
4de448d7
NG
75 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
76 . '<br />' .
77 '<a href="%2">Read more about this warning</a>';
78 $messages[] = new CRM_Utils_Check_Message(
165aab59 79 __FUNCTION__,
be2fb01f 80 ts($msg, [1 => $log_url, 2 => $docs_url]),
165aab59
CW
81 ts('Security Warning'),
82 \Psr\Log\LogLevel::WARNING,
83 'fa-lock'
4de448d7
NG
84 );
85 }
349b394e 86 }
5c58b447 87 }
349b394e 88 }
e7d3e318
TO
89
90 return $messages;
349b394e
CB
91 }
92
93 /**
94 * Check if our uploads directory has accessible files.
95 *
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.
99 *
100 * Being retrievable doesn't mean the files are browseable or visible
101 * to search engines; it only means they can be requested directly.
102 *
02b3ba91 103 * @return CRM_Utils_Check_Message[]
349b394e 104 * @see CRM-14091
5c58b447 105 *
50bfb460 106 * @todo Test with WordPress, Joomla.
349b394e 107 */
23d89616 108 public function checkUploadsAreNotAccessible() {
be2fb01f 109 $messages = [];
e7d3e318 110
349b394e 111 $config = CRM_Core_Config::singleton();
be2fb01f 112 $privateDirs = [
a738c74c
TO
113 $config->uploadDir,
114 $config->customFileUploadDir,
be2fb01f 115 ];
5c58b447 116
a738c74c
TO
117 foreach ($privateDirs as $privateDir) {
118 $heuristicUrl = $this->guessUrl($privateDir);
119 if ($this->isDirAccessible($privateDir, $heuristicUrl)) {
120 $messages[] = new CRM_Utils_Check_Message(
165aab59 121 __FUNCTION__,
a738c74c 122 ts('Files in the data directory (<a href="%3">%2</a>) should not be downloadable.'
353ffa53
TO
123 . '<br />'
124 . '<a href="%1">Read more about this warning</a>',
be2fb01f 125 [
4aae9070 126 1 => $this->createDocUrl('uploads-should-not-be-accessible'),
a738c74c
TO
127 2 => $privateDir,
128 3 => $heuristicUrl,
be2fb01f 129 ]),
4094935a
AP
130 ts('Private Files Readable'),
131 \Psr\Log\LogLevel::WARNING,
132 'fa-lock'
a738c74c 133 );
5c58b447 134 }
349b394e 135 }
e7d3e318
TO
136
137 return $messages;
349b394e
CB
138 }
139
140 /**
141 * Check if our uploads or ConfigAndLog directories have browseable
142 * listings.
143 *
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).
149 *
02b3ba91 150 * @return CRM_Utils_Check_Message[]
349b394e 151 * @see CRM-14091
5c58b447 152 *
50bfb460 153 * @todo Test with WordPress, Joomla.
349b394e 154 */
23d89616 155 public function checkDirectoriesAreNotBrowseable() {
be2fb01f 156 $messages = [];
349b394e 157 $config = CRM_Core_Config::singleton();
be2fb01f 158 $publicDirs = [
af5201d4 159 $config->imageUploadDir => $config->imageUploadURL,
be2fb01f 160 ];
af5201d4
TO
161
162 // Setup index.html files to prevent browsing
163 foreach ($publicDirs as $publicDir => $publicUrl) {
164 CRM_Utils_File::restrictBrowsing($publicDir);
165 }
166
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.'
171 . '<br />' .
172 '<a href="%3">Read more about this warning</a>';
4aae9070 173 $docs_url = $this->createDocUrl('directories-should-not-be-browsable');
a2600a6d 174 $messages[] = new CRM_Utils_Check_Message(
165aab59 175 __FUNCTION__,
be2fb01f 176 ts($msg, [1 => $publicDir, 2 => $publicDir, 3 => $docs_url]),
1b366958 177 ts('Browseable Directories'),
165aab59
CW
178 \Psr\Log\LogLevel::ERROR,
179 'fa-lock'
a2600a6d 180 );
5c58b447 181 }
349b394e 182 }
e7d3e318
TO
183
184 return $messages;
349b394e
CB
185 }
186
44d3eb82 187 /**
47b04697
TO
188 * Check that some files are not present.
189 *
190 * These files have generally been deleted but Civi source tree but could be
191 * left online if one does a faulty upgrade.
44d3eb82 192 *
02b3ba91 193 * @return CRM_Utils_Check_Message[]
44d3eb82
NG
194 */
195 public function checkFilesAreNotPresent() {
4dbfc1ba 196 $packages_path = rtrim(\Civi::paths()->getPath('[civicrm.packages]/'), '/' . DIRECTORY_SEPARATOR);
197 $vendor_path = rtrim(\Civi::paths()->getPath('[civicrm.vendor]/'), '/' . DIRECTORY_SEPARATOR);
44d3eb82 198
be2fb01f
CW
199 $messages = [];
200 $files = [
201 [
4d7bf881 202 // CRM-16005, upgraded from Civi <= 4.5.6
4dbfc1ba 203 "{$packages_path}/dompdf/dompdf.php",
7df9b5d5 204 \Psr\Log\LogLevel::CRITICAL,
be2fb01f
CW
205 ],
206 [
4d7bf881 207 // CRM-16005, Civi >= 4.5.7
4dbfc1ba 208 "{$packages_path}/vendor/dompdf/dompdf/dompdf.php",
7df9b5d5 209 \Psr\Log\LogLevel::CRITICAL,
be2fb01f
CW
210 ],
211 [
4d7bf881 212 // CRM-16005, Civi >= 4.6.0
4dbfc1ba 213 "{$vendor_path}/dompdf/dompdf/dompdf.php",
7df9b5d5 214 \Psr\Log\LogLevel::CRITICAL,
be2fb01f
CW
215 ],
216 [
4d7bf881 217 // CIVI-SA-2013-001
4dbfc1ba 218 "{$packages_path}/OpenFlashChart/php-ofc-library/ofc_upload_image.php",
7df9b5d5 219 \Psr\Log\LogLevel::CRITICAL,
be2fb01f
CW
220 ],
221 [
4dbfc1ba 222 "{$packages_path}/html2text/class.html2text.inc",
7df9b5d5 223 \Psr\Log\LogLevel::CRITICAL,
be2fb01f
CW
224 ],
225 ];
44d3eb82 226 foreach ($files as $file) {
7df9b5d5 227 if (file_exists($file[0])) {
44d3eb82 228 $messages[] = new CRM_Utils_Check_Message(
165aab59 229 __FUNCTION__,
be2fb01f 230 ts('File \'%1\' presents a security risk and should be deleted.', [1 => $file[0]]),
1b366958 231 ts('Unsafe Files'),
165aab59
CW
232 $file[1],
233 'fa-lock'
44d3eb82
NG
234 );
235 }
236 }
237 return $messages;
238 }
239
181e2dd5
TO
240 /**
241 * Discourage use of remote profile forms.
02b3ba91 242 * @return CRM_Utils_Check_Message[]
181e2dd5
TO
243 */
244 public function checkRemoteProfile() {
be2fb01f 245 $messages = [];
181e2dd5
TO
246
247 if (Civi::settings()->get('remote_profile_submissions')) {
248 $messages[] = new CRM_Utils_Check_Message(
249 __FUNCTION__,
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.',
be2fb01f 251 [1 => CRM_Utils_System::url('civicrm/admin/setting/misc', 'reset=1')]
181e2dd5
TO
252 ),
253 ts('Remote Profiles Enabled'),
254 \Psr\Log\LogLevel::WARNING,
255 'fa-lock'
256 );
257 }
258
259 return $messages;
260 }
261
098de400 262 /**
02b3ba91
CW
263 * Check that the sysadmin has not modified the Cxn security setup.
264 * @return CRM_Utils_Check_Message[]
098de400
TO
265 */
266 public function checkCxnOverrides() {
be2fb01f 267 $list = [];
098de400
TO
268 if (defined('CIVICRM_CXN_CA') && CIVICRM_CXN_CA !== 'CiviRootCA') {
269 $list[] = 'CIVICRM_CXN_CA';
270 }
098de400
TO
271 if (defined('CIVICRM_CXN_APPS_URL') && CIVICRM_CXN_APPS_URL !== \Civi\Cxn\Rpc\Constants::OFFICIAL_APPMETAS_URL) {
272 $list[] = 'CIVICRM_CXN_APPS_URL';
273 }
274
be2fb01f 275 $messages = [];
098de400
TO
276
277 if (!empty($list)) {
278 $messages[] = new CRM_Utils_Check_Message(
165aab59 279 __FUNCTION__,
be2fb01f 280 ts('The system administrator has disabled security settings (%1). Connections to remote applications are insecure.', [
098de400 281 1 => implode(', ', $list),
be2fb01f 282 ]),
165aab59
CW
283 ts('Security Warning'),
284 \Psr\Log\LogLevel::WARNING,
285 'fa-lock'
098de400
TO
286 );
287 }
288
289 return $messages;
290 }
291
af5201d4
TO
292 /**
293 * Determine whether $url is a public, browsable listing for $dir
294 *
77855840
TO
295 * @param string $dir
296 * Local dir path.
297 * @param string $url
298 * Public URL.
af5201d4
TO
299 * @return bool
300 */
301 public function isBrowsable($dir, $url) {
1305c22b 302 if (empty($dir) || empty($url) || !is_dir($dir)) {
a8488826
TO
303 return FALSE;
304 }
305
af5201d4 306 $result = FALSE;
af5201d4 307
a8488826 308 // this could be a new system with no uploads (yet) -- so we'll make a file
3f0e59f6
AH
309 $file = CRM_Utils_File::createFakeFile($dir);
310
311 if ($file === FALSE) {
312 // Couldn't write the file
313 return FALSE;
314 }
315
21acec2c 316 $content = @file_get_contents("$url");
af5201d4
TO
317 if (stristr($content, $file)) {
318 $result = TRUE;
319 }
320 unlink("$dir/$file");
321
322 return $result;
323 }
324
a738c74c
TO
325 /**
326 * Determine whether $url is a public version of $dir in which files
327 * are remotely accessible.
328 *
77855840
TO
329 * @param string $dir
330 * Local dir path.
331 * @param string $url
332 * Public URL.
a738c74c
TO
333 * @return bool
334 */
335 public function isDirAccessible($dir, $url) {
a738c74c
TO
336 $url = rtrim($url, '/');
337 if (empty($dir) || empty($url) || !is_dir($dir)) {
338 return FALSE;
339 }
340
341 $result = FALSE;
3f0e59f6 342 $file = CRM_Utils_File::createFakeFile($dir, 'delete me');
a738c74c 343
3f0e59f6
AH
344 if ($file === FALSE) {
345 // Couldn't write the file
346 return FALSE;
347 }
a738c74c 348
4094935a 349 if ($this->fileExists("$url/$file")) {
a738c74c
TO
350 $content = @file_get_contents("$url/$file");
351 if (preg_match('/delete me/', $content)) {
352 $result = TRUE;
353 }
354 }
355
356 unlink("$dir/$file");
357
358 return $result;
359 }
360
5bc392e6
EM
361 /**
362 * @param $topic
363 *
364 * @return string
365 */
7d342759 366 public function createDocUrl($topic) {
4aae9070 367 return CRM_Utils_System::docURL2('sysadmin/setup/security#' . $topic, TRUE);
7d342759 368 }
a738c74c
TO
369
370 /**
371 * Make a guess about the URL that corresponds to $targetDir.
372 *
77855840
TO
373 * @param string $targetDir
374 * Local path to a directory.
a6c01b45
CW
375 * @return string
376 * a guessed URL for $realDir
a738c74c
TO
377 */
378 public function guessUrl($targetDir) {
379 $filePathMarker = $this->getFilePathMarker();
380 $config = CRM_Core_Config::singleton();
381
980e227e
CW
382 list($heuristicBaseUrl) = explode($filePathMarker, $config->imageUploadURL);
383 list(, $heuristicSuffix) = array_pad(explode($filePathMarker, str_replace('\\', '/', $targetDir)), 2, '');
a738c74c
TO
384 return $heuristicBaseUrl . $filePathMarker . $heuristicSuffix;
385 }
96025800 386
349b394e 387}