copyright and version fixes
[civicrm-core.git] / CRM / Utils / Check / Security.php
CommitLineData
349b394e
CB
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
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-2014
32 * $Id: $
33 *
34 */
35class CRM_Utils_Check_Security {
36
6e663e84
CB
37 CONST
38 // How often to run checks and notify admins about issues.
39 CHECK_TIMER = 86400;
40
349b394e
CB
41 /**
42 * We only need one instance of this object, so we use the
43 * singleton pattern and cache the instance in this variable
44 *
45 * @var object
46 * @static
47 */
48 static private $_singleton = NULL;
49
50 /**
51 * Provide static instance of CRM_Utils_Check_Security.
52 *
53 * @return CRM_Utils_Check_Security
54 */
55 static function &singleton() {
56 if (!isset(self::$_singleton)) {
57 self::$_singleton = new CRM_Utils_Check_Security();
58 }
59 return self::$_singleton;
60 }
61
5c58b447
CB
62 /**
63 * CMS have a different pattern to their default file path and URL.
64 *
65 * @TODO This function might be better shared in CRM_Utils_Check
66 * class, but that class doesn't yet exist.
67 */
a996bf82 68 public function getFilePathMarker() {
5c58b447
CB
69 $config = CRM_Core_Config::singleton();
70 switch ($config->userFramework) {
71 case 'Joomla':
72 return '/media/';
73 default:
74 return '/files/';
75 }
76 }
77
9979ff93
TO
78 /**
79 * Execute "checkAll"
80 */
81 public function showPeriodicAlerts() {
439a9f1b
TO
82 if (CRM_Core_Permission::check('administer CiviCRM')
83 && CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'securityAlert', NULL, TRUE)
84 ) {
9979ff93
TO
85 $session = CRM_Core_Session::singleton();
86 if ($session->timer('check_' . __CLASS__, self::CHECK_TIMER)) {
fea6131e
TO
87
88 // Best attempt at re-securing folders
89 $config = CRM_Core_Config::singleton();
90 $config->cleanup(0, FALSE);
91
e7d3e318 92 foreach ($this->checkAll() as $message) {
a2600a6d 93 CRM_Core_Session::setStatus($message->getMessage(), ts('Security Warning'));
e7d3e318 94 }
9979ff93
TO
95 }
96 }
97 }
98
349b394e
CB
99 /**
100 * Run some sanity checks.
101 *
102 * This could become a hook so that CiviCRM can run both built-in
103 * configuration & sanity checks, and modules/extensions can add
104 * their own checks.
105 *
106 * We might even expose the results of these checks on the Wordpress
107 * plugin status page or the Drupal admin/reports/status path.
108 *
e7d3e318 109 * @return array of messages
349b394e
CB
110 * @see Drupal's hook_requirements() -
111 * https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_requirements
112 */
9979ff93 113 public function checkAll() {
e7d3e318
TO
114 $messages = array_merge(
115 CRM_Utils_Check_Security::singleton()->checkLogFileIsNotAccessible(),
116 CRM_Utils_Check_Security::singleton()->checkUploadsAreNotAccessible(),
117 CRM_Utils_Check_Security::singleton()->checkDirectoriesAreNotBrowseable()
118 );
119 return $messages;
349b394e
CB
120 }
121
122 /**
123 * Check if our logfile is directly accessible.
124 *
125 * Per CiviCRM default the logfile sits in a folder which is
126 * web-accessible, and is protected by a default .htaccess
127 * configuration. If server config causes the .htaccess not to
128 * function as intended, there may be information disclosure.
129 *
130 * The debug log may be jam-packed with sensitive data, we don't
131 * want that.
132 *
133 * Being able to be retrieved directly doesn't mean the logfile
134 * is browseable or visible to search engines; it means it can be
135 * requested directly.
136 *
e7d3e318 137 * @return array of messages
349b394e
CB
138 * @see CRM-14091
139 */
23d89616 140 public function checkLogFileIsNotAccessible() {
e7d3e318
TO
141 $messages = array();
142
349b394e
CB
143 $config = CRM_Core_Config::singleton();
144
145 $log = CRM_Core_Error::createDebugLogger();
146 $log_filename = $log->_filename;
147
a996bf82 148 $filePathMarker = $this->getFilePathMarker();
5c58b447 149
349b394e
CB
150 // Hazard a guess at the URL of the logfile, based on common
151 // CiviCRM layouts.
5c58b447
CB
152 if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) {
153 $url[] = $upload_url[0];
154 if ($log_path = explode($filePathMarker, $log_filename)) {
155 $url[] = $log_path[1];
156 $log_url = implode($filePathMarker, $url);
7d342759 157 $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible');
5ee70cb2
CB
158 $headers = @get_headers($log_url);
159 if (stripos($headers[0], '200')) {
5c58b447
CB
160 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
161 . '<br />' .
162 '<a href="%2">Read more about this warning</a>';
a2600a6d
TO
163 $messages[] = new CRM_Utils_Check_Message(
164 'checkLogFileIsNotAccessible',
165 ts($msg, array(1 => $log_url, 2 => $docs_url))
166 );
349b394e 167 }
5c58b447 168 }
349b394e 169 }
e7d3e318
TO
170
171 return $messages;
349b394e
CB
172 }
173
174 /**
175 * Check if our uploads directory has accessible files.
176 *
177 * We'll test a handful of files randomly. Hazard a guess at the URL
178 * of the uploads dir, based on common CiviCRM layouts. Try and
179 * request the files, and if any are successfully retrieved, warn.
180 *
181 * Being retrievable doesn't mean the files are browseable or visible
182 * to search engines; it only means they can be requested directly.
183 *
e7d3e318 184 * @return array of messages
349b394e 185 * @see CRM-14091
5c58b447
CB
186 *
187 * @TODO: Test with WordPress, Joomla.
349b394e 188 */
23d89616 189 public function checkUploadsAreNotAccessible() {
e7d3e318
TO
190 $messages = array();
191
349b394e 192 $config = CRM_Core_Config::singleton();
a996bf82 193 $filePathMarker = $this->getFilePathMarker();
5c58b447
CB
194
195 if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) {
196 if ($files = glob($config->uploadDir . '/*')) {
4ab6fe5d 197 for ($i = 0; $i < 3; $i++) {
5c58b447
CB
198 $f = array_rand($files);
199 if ($file_path = explode($filePathMarker, $files[$f])) {
200 $url = implode($filePathMarker, array($upload_url[0], $file_path[1]));
5ee70cb2
CB
201 $headers = @get_headers($url);
202 if (stripos($headers[0], '200')) {
5c58b447
CB
203 $msg = 'Files in the upload directory should not be downloadable.'
204 . '<br />' .
7d342759
TO
205 '<a href="%1">Read more about this warning</a>';
206 $docs_url = $this->createDocUrl('checkUploadsAreNotAccessible');
a2600a6d
TO
207 $messages[] = new CRM_Utils_Check_Message(
208 'checkUploadsAreNotAccessible',
209 ts($msg, array(1 => $docs_url))
210 );
349b394e
CB
211 }
212 }
213 }
5c58b447 214 }
349b394e 215 }
e7d3e318
TO
216
217 return $messages;
349b394e
CB
218 }
219
220 /**
221 * Check if our uploads or ConfigAndLog directories have browseable
222 * listings.
223 *
224 * Retrieve a listing of files from the local filesystem, and the
225 * corresponding path via HTTP. Then check and see if the local
226 * files are represented in the HTTP result; if so then warn. This
227 * MAY trigger false positives (if you have files named 'a', 'e'
228 * we'll probably match that).
229 *
e7d3e318 230 * @return array of messages
349b394e 231 * @see CRM-14091
5c58b447
CB
232 *
233 * @TODO: Test with WordPress, Joomla.
349b394e 234 */
23d89616 235 public function checkDirectoriesAreNotBrowseable() {
e7d3e318 236 $messages = array();
349b394e 237 $config = CRM_Core_Config::singleton();
af5201d4
TO
238 $publicDirs = array(
239 $config->imageUploadDir => $config->imageUploadURL,
5c58b447 240 );
af5201d4
TO
241
242 // Setup index.html files to prevent browsing
243 foreach ($publicDirs as $publicDir => $publicUrl) {
244 CRM_Utils_File::restrictBrowsing($publicDir);
245 }
246
247 // Test that $publicDir is not browsable
248 foreach ($publicDirs as $publicDir => $publicUrl) {
249 if ($this->isBrowsable($publicDir, $publicUrl)) {
250 $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.'
251 . '<br />' .
252 '<a href="%3">Read more about this warning</a>';
253 $docs_url = $this->createDocUrl('checkDirectoriesAreNotBrowseable');
a2600a6d
TO
254 $messages[] = new CRM_Utils_Check_Message(
255 'checkDirectoriesAreNotBrowseable',
256 ts($msg, array(1 => $publicDir, 2 => $publicDir, 3 => $docs_url))
257 );
5c58b447 258 }
349b394e 259 }
e7d3e318
TO
260
261 return $messages;
349b394e
CB
262 }
263
af5201d4
TO
264 /**
265 * Determine whether $url is a public, browsable listing for $dir
266 *
267 * @param string $dir local dir path
268 * @param string $url public URL
269 * @return bool
270 */
271 public function isBrowsable($dir, $url) {
1305c22b 272 if (empty($dir) || empty($url) || !is_dir($dir)) {
a8488826
TO
273 return FALSE;
274 }
275
af5201d4
TO
276 $result = FALSE;
277 $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
278
a8488826 279 // this could be a new system with no uploads (yet) -- so we'll make a file
af5201d4 280 file_put_contents("$dir/$file", "delete me");
21acec2c 281 $content = @file_get_contents("$url");
af5201d4
TO
282 if (stristr($content, $file)) {
283 $result = TRUE;
284 }
285 unlink("$dir/$file");
286
287 return $result;
288 }
289
7d342759
TO
290 public function createDocUrl($topic) {
291 return CRM_Utils_System::getWikiBaseURL() . $topic;
292 }
349b394e 293}