3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.4 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2014
35 class CRM_Utils_Check_Security
{
38 // How often to run checks and notify admins about issues.
42 * We only need one instance of this object, so we use the
43 * singleton pattern and cache the instance in this variable
48 static private $_singleton = NULL;
51 * Provide static instance of CRM_Utils_Check_Security.
53 * @return CRM_Utils_Check_Security
55 static function &singleton() {
56 if (!isset(self
::$_singleton)) {
57 self
::$_singleton = new CRM_Utils_Check_Security();
59 return self
::$_singleton;
63 * CMS have a different pattern to their default file path and URL.
65 * @TODO This function might be better shared in CRM_Utils_Check
66 * class, but that class doesn't yet exist.
68 public function getFilePathMarker() {
69 $config = CRM_Core_Config
::singleton();
70 switch ($config->userFramework
) {
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
)) {
86 // Best attempt at re-securing folders
87 $config = CRM_Core_Config
::singleton();
88 $config->cleanup(0, FALSE);
90 foreach ($this->checkAll() as $message) {
91 CRM_Core_Session
::setStatus($message, ts('Security Warning'));
98 * Run some sanity checks.
100 * This could become a hook so that CiviCRM can run both built-in
101 * configuration & sanity checks, and modules/extensions can add
104 * We might even expose the results of these checks on the Wordpress
105 * plugin status page or the Drupal admin/reports/status path.
107 * @return array of messages
108 * @see Drupal's hook_requirements() -
109 * https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_requirements
111 public function checkAll() {
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()
121 * Check if our logfile is directly accessible.
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.
128 * The debug log may be jam-packed with sensitive data, we don't
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.
135 * @return array of messages
138 public function checkLogFileIsNotAccessible() {
141 $config = CRM_Core_Config
::singleton();
143 $log = CRM_Core_Error
::createDebugLogger();
144 $log_filename = $log->_filename
;
146 $filePathMarker = $this->getFilePathMarker();
148 // Hazard a guess at the URL of the logfile, based on common
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);
155 $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible');
156 if ($log = @file_get_contents
($log_url)) {
157 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
159 '<a href="%2">Read more about this warning</a>';
160 $messages[] = ts($msg, array(1 => $log_url, 2 => $docs_url));
169 * Check if our uploads directory has accessible files.
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.
175 * Being retrievable doesn't mean the files are browseable or visible
176 * to search engines; it only means they can be requested directly.
178 * @return array of messages
181 * @TODO: Test with WordPress, Joomla.
183 public function checkUploadsAreNotAccessible() {
186 $config = CRM_Core_Config
::singleton();
187 $filePathMarker = $this->getFilePathMarker();
189 if ($upload_url = explode($filePathMarker, $config->imageUploadURL
)) {
190 if ($files = glob($config->uploadDir
. '/*')) {
191 for ($i = 0; $i < 3; $i++
) {
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.'
198 '<a href="%1">Read more about this warning</a>';
199 $docs_url = $this->createDocUrl('checkUploadsAreNotAccessible');
200 $messages[] = ts($msg, array(1 => $docs_url));
211 * Check if our uploads or ConfigAndLog directories have browseable
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).
220 * @return array of messages
223 * @TODO: Test with WordPress, Joomla.
225 public function checkDirectoriesAreNotBrowseable() {
227 $config = CRM_Core_Config
::singleton();
229 $config->imageUploadDir
=> $config->imageUploadURL
,
232 // Setup index.html files to prevent browsing
233 foreach ($publicDirs as $publicDir => $publicUrl) {
234 CRM_Utils_File
::restrictBrowsing($publicDir);
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.'
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));
252 * Determine whether $url is a public, browsable listing for $dir
254 * @param string $dir local dir path
255 * @param string $url public URL
258 public function isBrowsable($dir, $url) {
259 if (empty($dir) ||
empty($url)) {
264 $file = 'delete-this-' . CRM_Utils_String
::createRandom(10, CRM_Utils_String
::ALPHANUMERIC
);
266 // this could be a new system with no uploads (yet) -- so we'll make a file
267 file_put_contents("$dir/$file", "delete me");
268 $content = @file_get_contents
("$url");
269 if (stristr($content, $file)) {
272 unlink("$dir/$file");
277 public function createDocUrl($topic) {
278 return CRM_Utils_System
::getWikiBaseURL() . $topic;