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