Merge pull request #3543 from eileenmcnaughton/CRM-14850
[civicrm-core.git] / CRM / Utils / Check / Security.php
CommitLineData
349b394e
CB
1<?php
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
349b394e
CB
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
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/';
48 default:
49 return '/files/';
50 }
51 }
52
349b394e
CB
53 /**
54 * Run some sanity checks.
55 *
1248c859 56 * @return array<CRM_Utils_Check_Message>
349b394e 57 */
9979ff93 58 public function checkAll() {
e7d3e318 59 $messages = array_merge(
1248c859
TO
60 $this->checkLogFileIsNotAccessible(),
61 $this->checkUploadsAreNotAccessible(),
62 $this->checkDirectoriesAreNotBrowseable()
e7d3e318
TO
63 );
64 return $messages;
349b394e
CB
65 }
66
67 /**
68 * Check if our logfile is directly accessible.
69 *
70 * Per CiviCRM default the logfile sits in a folder which is
71 * web-accessible, and is protected by a default .htaccess
72 * configuration. If server config causes the .htaccess not to
73 * function as intended, there may be information disclosure.
74 *
75 * The debug log may be jam-packed with sensitive data, we don't
76 * want that.
77 *
78 * Being able to be retrieved directly doesn't mean the logfile
79 * is browseable or visible to search engines; it means it can be
80 * requested directly.
81 *
e7d3e318 82 * @return array of messages
349b394e
CB
83 * @see CRM-14091
84 */
23d89616 85 public function checkLogFileIsNotAccessible() {
e7d3e318
TO
86 $messages = array();
87
349b394e
CB
88 $config = CRM_Core_Config::singleton();
89
90 $log = CRM_Core_Error::createDebugLogger();
91 $log_filename = $log->_filename;
92
a996bf82 93 $filePathMarker = $this->getFilePathMarker();
5c58b447 94
349b394e
CB
95 // Hazard a guess at the URL of the logfile, based on common
96 // CiviCRM layouts.
5c58b447
CB
97 if ($upload_url = explode($filePathMarker, $config->imageUploadURL)) {
98 $url[] = $upload_url[0];
99 if ($log_path = explode($filePathMarker, $log_filename)) {
100 $url[] = $log_path[1];
101 $log_url = implode($filePathMarker, $url);
5ee70cb2
CB
102 $headers = @get_headers($log_url);
103 if (stripos($headers[0], '200')) {
a738c74c 104 $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible');
5c58b447
CB
105 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
106 . '<br />' .
107 '<a href="%2">Read more about this warning</a>';
a2600a6d
TO
108 $messages[] = new CRM_Utils_Check_Message(
109 'checkLogFileIsNotAccessible',
1248c859
TO
110 ts($msg, array(1 => $log_url, 2 => $docs_url)),
111 ts('Security Warning')
a2600a6d 112 );
349b394e 113 }
5c58b447 114 }
349b394e 115 }
e7d3e318
TO
116
117 return $messages;
349b394e
CB
118 }
119
120 /**
121 * Check if our uploads directory has accessible files.
122 *
123 * We'll test a handful of files randomly. Hazard a guess at the URL
124 * of the uploads dir, based on common CiviCRM layouts. Try and
125 * request the files, and if any are successfully retrieved, warn.
126 *
127 * Being retrievable doesn't mean the files are browseable or visible
128 * to search engines; it only means they can be requested directly.
129 *
e7d3e318 130 * @return array of messages
349b394e 131 * @see CRM-14091
5c58b447
CB
132 *
133 * @TODO: Test with WordPress, Joomla.
349b394e 134 */
23d89616 135 public function checkUploadsAreNotAccessible() {
e7d3e318
TO
136 $messages = array();
137
349b394e 138 $config = CRM_Core_Config::singleton();
a738c74c
TO
139 $privateDirs = array(
140 $config->uploadDir,
141 $config->customFileUploadDir,
142 );
5c58b447 143
a738c74c
TO
144 foreach ($privateDirs as $privateDir) {
145 $heuristicUrl = $this->guessUrl($privateDir);
146 if ($this->isDirAccessible($privateDir, $heuristicUrl)) {
147 $messages[] = new CRM_Utils_Check_Message(
148 'checkUploadsAreNotAccessible',
149 ts('Files in the data directory (<a href="%3">%2</a>) should not be downloadable.'
150 . '<br />'
151 . '<a href="%1">Read more about this warning</a>',
152 array(
153 1 => $this->createDocUrl('checkUploadsAreNotAccessible'),
154 2 => $privateDir,
155 3 => $heuristicUrl,
156 )),
157 ts('Security Warning')
158 );
5c58b447 159 }
349b394e 160 }
e7d3e318
TO
161
162 return $messages;
349b394e
CB
163 }
164
165 /**
166 * Check if our uploads or ConfigAndLog directories have browseable
167 * listings.
168 *
169 * Retrieve a listing of files from the local filesystem, and the
170 * corresponding path via HTTP. Then check and see if the local
171 * files are represented in the HTTP result; if so then warn. This
172 * MAY trigger false positives (if you have files named 'a', 'e'
173 * we'll probably match that).
174 *
e7d3e318 175 * @return array of messages
349b394e 176 * @see CRM-14091
5c58b447
CB
177 *
178 * @TODO: Test with WordPress, Joomla.
349b394e 179 */
23d89616 180 public function checkDirectoriesAreNotBrowseable() {
e7d3e318 181 $messages = array();
349b394e 182 $config = CRM_Core_Config::singleton();
af5201d4
TO
183 $publicDirs = array(
184 $config->imageUploadDir => $config->imageUploadURL,
5c58b447 185 );
af5201d4
TO
186
187 // Setup index.html files to prevent browsing
188 foreach ($publicDirs as $publicDir => $publicUrl) {
189 CRM_Utils_File::restrictBrowsing($publicDir);
190 }
191
192 // Test that $publicDir is not browsable
193 foreach ($publicDirs as $publicDir => $publicUrl) {
194 if ($this->isBrowsable($publicDir, $publicUrl)) {
195 $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.'
196 . '<br />' .
197 '<a href="%3">Read more about this warning</a>';
198 $docs_url = $this->createDocUrl('checkDirectoriesAreNotBrowseable');
a2600a6d
TO
199 $messages[] = new CRM_Utils_Check_Message(
200 'checkDirectoriesAreNotBrowseable',
1248c859
TO
201 ts($msg, array(1 => $publicDir, 2 => $publicDir, 3 => $docs_url)),
202 ts('Security Warning')
a2600a6d 203 );
5c58b447 204 }
349b394e 205 }
e7d3e318
TO
206
207 return $messages;
349b394e
CB
208 }
209
af5201d4
TO
210 /**
211 * Determine whether $url is a public, browsable listing for $dir
212 *
213 * @param string $dir local dir path
214 * @param string $url public URL
215 * @return bool
216 */
217 public function isBrowsable($dir, $url) {
1305c22b 218 if (empty($dir) || empty($url) || !is_dir($dir)) {
a8488826
TO
219 return FALSE;
220 }
221
af5201d4
TO
222 $result = FALSE;
223 $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
224
a8488826 225 // this could be a new system with no uploads (yet) -- so we'll make a file
af5201d4 226 file_put_contents("$dir/$file", "delete me");
21acec2c 227 $content = @file_get_contents("$url");
af5201d4
TO
228 if (stristr($content, $file)) {
229 $result = TRUE;
230 }
231 unlink("$dir/$file");
232
233 return $result;
234 }
235
a738c74c
TO
236 /**
237 * Determine whether $url is a public version of $dir in which files
238 * are remotely accessible.
239 *
240 * @param string $dir local dir path
241 * @param string $url public URL
242 * @return bool
243 */
244 public function isDirAccessible($dir, $url) {
245 $dir = rtrim($dir, '/');
246 $url = rtrim($url, '/');
247 if (empty($dir) || empty($url) || !is_dir($dir)) {
248 return FALSE;
249 }
250
251 $result = FALSE;
252 $file = 'delete-this-' . CRM_Utils_String::createRandom(10, CRM_Utils_String::ALPHANUMERIC);
253
254 // this could be a new system with no uploads (yet) -- so we'll make a file
255 file_put_contents("$dir/$file", "delete me");
256
257 $headers = @get_headers("$url/$file");
258 if (stripos($headers[0], '200')) {
259 $content = @file_get_contents("$url/$file");
260 if (preg_match('/delete me/', $content)) {
261 $result = TRUE;
262 }
263 }
264
265 unlink("$dir/$file");
266
267 return $result;
268 }
269
5bc392e6
EM
270 /**
271 * @param $topic
272 *
273 * @return string
274 */
7d342759
TO
275 public function createDocUrl($topic) {
276 return CRM_Utils_System::getWikiBaseURL() . $topic;
277 }
a738c74c
TO
278
279 /**
280 * Make a guess about the URL that corresponds to $targetDir.
281 *
282 * @param string $targetDir local path to a directory
283 * @return string a guessed URL for $realDir
284 */
285 public function guessUrl($targetDir) {
286 $filePathMarker = $this->getFilePathMarker();
287 $config = CRM_Core_Config::singleton();
288
289 list ($heuristicBaseUrl, $ignore) = explode($filePathMarker, $config->imageUploadURL);
290 list ($ignore, $heuristicSuffix) = explode($filePathMarker, $targetDir);
291 return $heuristicBaseUrl . $filePathMarker . $heuristicSuffix;
292 }
349b394e 293}