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