Commit | Line | Data |
---|---|---|
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 | */ | |
35 | class 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() { | |
82 | if (CRM_Core_Permission::check('administer CiviCRM')) { | |
83 | $session = CRM_Core_Session::singleton(); | |
84 | if ($session->timer('check_' . __CLASS__, self::CHECK_TIMER)) { | |
fea6131e TO |
85 | |
86 | // Best attempt at re-securing folders | |
87 | $config = CRM_Core_Config::singleton(); | |
88 | $config->cleanup(0, FALSE); | |
89 | ||
e7d3e318 TO |
90 | foreach ($this->checkAll() as $message) { |
91 | CRM_Core_Session::setStatus($message, ts('Security Warning')); | |
92 | } | |
9979ff93 TO |
93 | } |
94 | } | |
95 | } | |
96 | ||
349b394e CB |
97 | /** |
98 | * Run some sanity checks. | |
99 | * | |
100 | * This could become a hook so that CiviCRM can run both built-in | |
101 | * configuration & sanity checks, and modules/extensions can add | |
102 | * their own checks. | |
103 | * | |
104 | * We might even expose the results of these checks on the Wordpress | |
105 | * plugin status page or the Drupal admin/reports/status path. | |
106 | * | |
e7d3e318 | 107 | * @return array of messages |
349b394e CB |
108 | * @see Drupal's hook_requirements() - |
109 | * https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_requirements | |
110 | */ | |
9979ff93 | 111 | public function checkAll() { |
e7d3e318 TO |
112 | $messages = array_merge( |
113 | CRM_Utils_Check_Security::singleton()->checkLogFileIsNotAccessible(), | |
114 | CRM_Utils_Check_Security::singleton()->checkUploadsAreNotAccessible(), | |
115 | CRM_Utils_Check_Security::singleton()->checkDirectoriesAreNotBrowseable() | |
116 | ); | |
117 | return $messages; | |
349b394e CB |
118 | } |
119 | ||
120 | /** | |
121 | * Check if our logfile is directly accessible. | |
122 | * | |
123 | * Per CiviCRM default the logfile sits in a folder which is | |
124 | * web-accessible, and is protected by a default .htaccess | |
125 | * configuration. If server config causes the .htaccess not to | |
126 | * function as intended, there may be information disclosure. | |
127 | * | |
128 | * The debug log may be jam-packed with sensitive data, we don't | |
129 | * want that. | |
130 | * | |
131 | * Being able to be retrieved directly doesn't mean the logfile | |
132 | * is browseable or visible to search engines; it means it can be | |
133 | * requested directly. | |
134 | * | |
e7d3e318 | 135 | * @return array of messages |
349b394e CB |
136 | * @see CRM-14091 |
137 | */ | |
23d89616 | 138 | public function checkLogFileIsNotAccessible() { |
e7d3e318 TO |
139 | $messages = array(); |
140 | ||
349b394e CB |
141 | $config = CRM_Core_Config::singleton(); |
142 | ||
143 | $log = CRM_Core_Error::createDebugLogger(); | |
144 | $log_filename = $log->_filename; | |
145 | ||
a996bf82 | 146 | $filePathMarker = $this->getFilePathMarker(); |
5c58b447 | 147 | |
349b394e CB |
148 | // Hazard a guess at the URL of the logfile, based on common |
149 | // CiviCRM layouts. | |
5c58b447 CB |
150 | if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) { |
151 | $url[] = $upload_url[0]; | |
152 | if ($log_path = explode($filePathMarker, $log_filename)) { | |
153 | $url[] = $log_path[1]; | |
154 | $log_url = implode($filePathMarker, $url); | |
7d342759 | 155 | $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible'); |
5c58b447 CB |
156 | if ($log = @file_get_contents($log_url)) { |
157 | $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.' | |
158 | . '<br />' . | |
159 | '<a href="%2">Read more about this warning</a>'; | |
e7d3e318 | 160 | $messages[] = ts($msg, array(1 => $log_url, 2 => $docs_url)); |
349b394e | 161 | } |
5c58b447 | 162 | } |
349b394e | 163 | } |
e7d3e318 TO |
164 | |
165 | return $messages; | |
349b394e CB |
166 | } |
167 | ||
168 | /** | |
169 | * Check if our uploads directory has accessible files. | |
170 | * | |
171 | * We'll test a handful of files randomly. Hazard a guess at the URL | |
172 | * of the uploads dir, based on common CiviCRM layouts. Try and | |
173 | * request the files, and if any are successfully retrieved, warn. | |
174 | * | |
175 | * Being retrievable doesn't mean the files are browseable or visible | |
176 | * to search engines; it only means they can be requested directly. | |
177 | * | |
e7d3e318 | 178 | * @return array of messages |
349b394e | 179 | * @see CRM-14091 |
5c58b447 CB |
180 | * |
181 | * @TODO: Test with WordPress, Joomla. | |
349b394e | 182 | */ |
23d89616 | 183 | public function checkUploadsAreNotAccessible() { |
e7d3e318 TO |
184 | $messages = array(); |
185 | ||
349b394e | 186 | $config = CRM_Core_Config::singleton(); |
a996bf82 | 187 | $filePathMarker = $this->getFilePathMarker(); |
5c58b447 CB |
188 | |
189 | if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) { | |
190 | if ($files = glob($config->uploadDir . '/*')) { | |
4ab6fe5d | 191 | for ($i = 0; $i < 3; $i++) { |
5c58b447 CB |
192 | $f = array_rand($files); |
193 | if ($file_path = explode($filePathMarker, $files[$f])) { | |
194 | $url = implode($filePathMarker, array($upload_url[0], $file_path[1])); | |
195 | if ($file = @file_get_contents($url)) { | |
196 | $msg = 'Files in the upload directory should not be downloadable.' | |
197 | . '<br />' . | |
7d342759 TO |
198 | '<a href="%1">Read more about this warning</a>'; |
199 | $docs_url = $this->createDocUrl('checkUploadsAreNotAccessible'); | |
e7d3e318 | 200 | $messages[] = ts($msg, array(1 => $docs_url)); |
349b394e CB |
201 | } |
202 | } | |
203 | } | |
5c58b447 | 204 | } |
349b394e | 205 | } |
e7d3e318 TO |
206 | |
207 | return $messages; | |
349b394e CB |
208 | } |
209 | ||
210 | /** | |
211 | * Check if our uploads or ConfigAndLog directories have browseable | |
212 | * listings. | |
213 | * | |
214 | * Retrieve a listing of files from the local filesystem, and the | |
215 | * corresponding path via HTTP. Then check and see if the local | |
216 | * files are represented in the HTTP result; if so then warn. This | |
217 | * MAY trigger false positives (if you have files named 'a', 'e' | |
218 | * we'll probably match that). | |
219 | * | |
e7d3e318 | 220 | * @return array of messages |
349b394e | 221 | * @see CRM-14091 |
5c58b447 CB |
222 | * |
223 | * @TODO: Test with WordPress, Joomla. | |
349b394e | 224 | */ |
23d89616 | 225 | public function checkDirectoriesAreNotBrowseable() { |
e7d3e318 | 226 | $messages = array(); |
349b394e | 227 | $config = CRM_Core_Config::singleton(); |
af5201d4 TO |
228 | $publicDirs = array( |
229 | $config->imageUploadDir => $config->imageUploadURL, | |
5c58b447 | 230 | ); |
af5201d4 TO |
231 | |
232 | // Setup index.html files to prevent browsing | |
233 | foreach ($publicDirs as $publicDir => $publicUrl) { | |
234 | CRM_Utils_File::restrictBrowsing($publicDir); | |
235 | } | |
236 | ||
237 | // Test that $publicDir is not browsable | |
238 | foreach ($publicDirs as $publicDir => $publicUrl) { | |
239 | if ($this->isBrowsable($publicDir, $publicUrl)) { | |
240 | $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.' | |
241 | . '<br />' . | |
242 | '<a href="%3">Read more about this warning</a>'; | |
243 | $docs_url = $this->createDocUrl('checkDirectoriesAreNotBrowseable'); | |
244 | $messages[] = ts($msg, array(1 => $publicDir, 2 => $publicDir, 3 => $docs_url)); | |
5c58b447 | 245 | } |
349b394e | 246 | } |
e7d3e318 TO |
247 | |
248 | return $messages; | |
349b394e CB |
249 | } |
250 | ||
af5201d4 TO |
251 | /** |
252 | * Determine whether $url is a public, browsable listing for $dir | |
253 | * | |
254 | * @param string $dir local dir path | |
255 | * @param string $url public URL | |
256 | * @return bool | |
257 | */ | |
258 | public function isBrowsable($dir, $url) { | |
a8488826 TO |
259 | if (empty($dir) || empty($url)) { |
260 | return FALSE; | |
261 | } | |
262 | ||
af5201d4 TO |
263 | $result = FALSE; |
264 | $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC); | |
265 | ||
a8488826 | 266 | // this could be a new system with no uploads (yet) -- so we'll make a file |
af5201d4 | 267 | file_put_contents("$dir/$file", "delete me"); |
21acec2c | 268 | $content = @file_get_contents("$url"); |
af5201d4 TO |
269 | if (stristr($content, $file)) { |
270 | $result = TRUE; | |
271 | } | |
272 | unlink("$dir/$file"); | |
273 | ||
274 | return $result; | |
275 | } | |
276 | ||
7d342759 TO |
277 | public function createDocUrl($topic) { |
278 | return CRM_Utils_System::getWikiBaseURL() . $topic; | |
279 | } | |
349b394e | 280 | } |