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