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