Merge remote-tracking branch 'upstream/4.6' into 4.6-master-2015-09-05-18-12-52
[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 * $Id: $
33 *
34 */
35 class CRM_Utils_Check_Security {
36
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 */
43 public function getFilePathMarker() {
44 $config = CRM_Core_Config::singleton();
45 switch ($config->userFramework) {
46 case 'Joomla':
47 return '/media/';
48
49 default:
50 return '/files/';
51 }
52 }
53
54 /**
55 * Run some sanity checks.
56 *
57 * @return array<CRM_Utils_Check_Message>
58 */
59 public function checkAll() {
60 $messages = array_merge(
61 $this->checkCxnOverrides(),
62 $this->checkLogFileIsNotAccessible(),
63 $this->checkUploadsAreNotAccessible(),
64 $this->checkDirectoriesAreNotBrowseable(),
65 $this->checkFilesAreNotPresent()
66 );
67 return $messages;
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 *
85 * @return array
86 * Array of messages
87 * @see CRM-14091
88 */
89 public function checkLogFileIsNotAccessible() {
90 $messages = array();
91
92 $config = CRM_Core_Config::singleton();
93
94 $log = CRM_Core_Error::createDebugLogger();
95 $log_filename = str_replace('\\', '/', $log->_filename);
96
97 $filePathMarker = $this->getFilePathMarker();
98
99 // Hazard a guess at the URL of the logfile, based on common
100 // CiviCRM layouts.
101 if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) {
102 $url[] = $upload_url[0];
103 if ($log_path = explode($filePathMarker, $log_filename)) {
104 // CRM-17149: check if debug log path includes $filePathMarker
105 if (count($log_path) > 1) {
106 $url[] = $log_path[1];
107 $log_url = implode($filePathMarker, $url);
108 $headers = @get_headers($log_url);
109 if (stripos($headers[0], '200')) {
110 $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible');
111 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
112 . '<br />' .
113 '<a href="%2">Read more about this warning</a>';
114 $messages[] = new CRM_Utils_Check_Message(
115 'checkLogFileIsNotAccessible',
116 ts($msg, array(1 => $log_url, 2 => $docs_url)),
117 ts('Security Warning')
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 'checkUploadsAreNotAccessible',
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 );
168 }
169 }
170
171 return $messages;
172 }
173
174 /**
175 * Check if our uploads or ConfigAndLog directories have browseable
176 * listings.
177 *
178 * Retrieve a listing of files from the local filesystem, and the
179 * corresponding path via HTTP. Then check and see if the local
180 * files are represented in the HTTP result; if so then warn. This
181 * MAY trigger false positives (if you have files named 'a', 'e'
182 * we'll probably match that).
183 *
184 * @return array
185 * Array of messages
186 * @see CRM-14091
187 *
188 * @TODO: Test with WordPress, Joomla.
189 */
190 public function checkDirectoriesAreNotBrowseable() {
191 $messages = array();
192 $config = CRM_Core_Config::singleton();
193 $publicDirs = array(
194 $config->imageUploadDir => $config->imageUploadURL,
195 );
196
197 // Setup index.html files to prevent browsing
198 foreach ($publicDirs as $publicDir => $publicUrl) {
199 CRM_Utils_File::restrictBrowsing($publicDir);
200 }
201
202 // Test that $publicDir is not browsable
203 foreach ($publicDirs as $publicDir => $publicUrl) {
204 if ($this->isBrowsable($publicDir, $publicUrl)) {
205 $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.'
206 . '<br />' .
207 '<a href="%3">Read more about this warning</a>';
208 $docs_url = $this->createDocUrl('checkDirectoriesAreNotBrowseable');
209 $messages[] = new CRM_Utils_Check_Message(
210 'checkDirectoriesAreNotBrowseable',
211 ts($msg, array(1 => $publicDir, 2 => $publicDir, 3 => $docs_url)),
212 ts('Browseable Directories'),
213 \Psr\Log\LogLevel::ERROR
214 );
215 }
216 }
217
218 return $messages;
219 }
220
221
222 /**
223 * Check that some files are not present.
224 *
225 * These files have generally been deleted but Civi source tree but could be
226 * left online if one does a faulty upgrade.
227 *
228 * @return array of messages
229 */
230 public function checkFilesAreNotPresent() {
231 global $civicrm_root;
232
233 $messages = array();
234 $files = array(
235 array(
236 "{$civicrm_root}/packages/dompdf/dompdf.php", // CRM-16005, upgraded from Civi <= 4.5.6
237 \Psr\Log\LogLevel::CRITICAL,
238 ),
239 array(
240 "{$civicrm_root}/packages/vendor/dompdf/dompdf/dompdf.php", // CRM-16005, Civi >= 4.5.7
241 \Psr\Log\LogLevel::CRITICAL,
242 ),
243 array(
244 "{$civicrm_root}/vendor/dompdf/dompdf/dompdf.php", // CRM-16005, Civi >= 4.6.0
245 \Psr\Log\LogLevel::CRITICAL,
246 ),
247 array(
248 "{$civicrm_root}/packages/OpenFlashChart/php-ofc-library/ofc_upload_image.php", // CIVI-SA-2013-001
249 \Psr\Log\LogLevel::CRITICAL,
250 ),
251 array(
252 "{$civicrm_root}/packages/html2text/class.html2text.inc",
253 \Psr\Log\LogLevel::CRITICAL,
254 ),
255 );
256 foreach ($files as $file) {
257 if (file_exists($file[0])) {
258 $messages[] = new CRM_Utils_Check_Message(
259 'checkFilesAreNotPresent',
260 ts('File \'%1\' presents a security risk and should be deleted.', array(1 => $file)),
261 ts('Unsafe Files'),
262 $file[1]
263 );
264 }
265 }
266 return $messages;
267 }
268
269 /**
270 * Check that the sysadmin has not modified the Cxn
271 * security setup.
272 */
273 public function checkCxnOverrides() {
274 $list = array();
275 if (defined('CIVICRM_CXN_CA') && CIVICRM_CXN_CA !== 'CiviRootCA') {
276 $list[] = 'CIVICRM_CXN_CA';
277 }
278 if (defined('CIVICRM_CXN_APPS_URL') && CIVICRM_CXN_APPS_URL !== \Civi\Cxn\Rpc\Constants::OFFICIAL_APPMETAS_URL) {
279 $list[] = 'CIVICRM_CXN_APPS_URL';
280 }
281
282 $messages = array();
283
284 if (!empty($list)) {
285 $messages[] = new CRM_Utils_Check_Message(
286 'checkCxnOverrides',
287 ts('The system administrator has disabled security settings (%1). Connections to remote applications are insecure.', array(
288 1 => implode(', ', $list),
289 )),
290 ts('Security Warning')
291 );
292 }
293
294 return $messages;
295 }
296
297 /**
298 * Determine whether $url is a public, browsable listing for $dir
299 *
300 * @param string $dir
301 * Local dir path.
302 * @param string $url
303 * Public URL.
304 * @return bool
305 */
306 public function isBrowsable($dir, $url) {
307 if (empty($dir) || empty($url) || !is_dir($dir)) {
308 return FALSE;
309 }
310
311 $result = FALSE;
312 $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
313
314 // this could be a new system with no uploads (yet) -- so we'll make a file
315 file_put_contents("$dir/$file", "delete me");
316 $content = @file_get_contents("$url");
317 if (stristr($content, $file)) {
318 $result = TRUE;
319 }
320 unlink("$dir/$file");
321
322 return $result;
323 }
324
325 /**
326 * Determine whether $url is a public version of $dir in which files
327 * are remotely accessible.
328 *
329 * @param string $dir
330 * Local dir path.
331 * @param string $url
332 * Public URL.
333 * @return bool
334 */
335 public function isDirAccessible($dir, $url) {
336 $dir = rtrim($dir, '/');
337 $url = rtrim($url, '/');
338 if (empty($dir) || empty($url) || !is_dir($dir)) {
339 return FALSE;
340 }
341
342 $result = FALSE;
343 $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
344
345 // this could be a new system with no uploads (yet) -- so we'll make a file
346 file_put_contents("$dir/$file", "delete me");
347
348 $headers = @get_headers("$url/$file");
349 if (stripos($headers[0], '200')) {
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
361 /**
362 * @param $topic
363 *
364 * @return string
365 */
366 public function createDocUrl($topic) {
367 return CRM_Utils_System::getWikiBaseURL() . $topic;
368 }
369
370 /**
371 * Make a guess about the URL that corresponds to $targetDir.
372 *
373 * @param string $targetDir
374 * Local path to a directory.
375 * @return string
376 * a guessed URL for $realDir
377 */
378 public function guessUrl($targetDir) {
379 $filePathMarker = $this->getFilePathMarker();
380 $config = CRM_Core_Config::singleton();
381
382 list ($heuristicBaseUrl, $ignore) = explode($filePathMarker, $config->imageUploadURL);
383 list ($ignore, $heuristicSuffix) = explode($filePathMarker, str_replace('\\', '/', $targetDir));
384 return $heuristicBaseUrl . $filePathMarker . $heuristicSuffix;
385 }
386
387 }