CRM-14091. Look for obvious misconfigurations and warn.
authorChris Burgess <chris@fuzion.co.nz>
Thu, 30 Jan 2014 10:42:28 +0000 (23:42 +1300)
committerTim Otten <totten@civicrm.org>
Wed, 5 Feb 2014 17:47:24 +0000 (09:47 -0800)
CRM/Core/Page.php
CRM/Utils/Check/Security.php [new file with mode: 0644]

index 95ff5575788cfb1e070f0ae980556e11bebddec3..4fc023cac4504154d2c54e56c02f9ff0a986ee81 100644 (file)
@@ -195,6 +195,7 @@ class CRM_Core_Page {
     if (empty($_GET['snippet'])) {
       // Version check and intermittent alert to admins
       CRM_Utils_VersionCheck::singleton()->versionAlert();
+      CRM_Utils_Check_Security::singleton()->allChecks();
 
       // Debug msg once per hour
       if ($config->debug && CRM_Core_Permission::check('administer CiviCRM') && CRM_Core_Session::singleton()->timer('debug_alert', 3600)) {
diff --git a/CRM/Utils/Check/Security.php b/CRM/Utils/Check/Security.php
new file mode 100644 (file)
index 0000000..ab554cf
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.4                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2014                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2014
+ * $Id: $
+ *
+ */
+class CRM_Utils_Check_Security {
+
+  /**
+   * We only need one instance of this object, so we use the
+   * singleton pattern and cache the instance in this variable
+   *
+   * @var object
+   * @static
+   */
+  static private $_singleton = NULL;
+
+  /**
+   * Provide static instance of CRM_Utils_Check_Security.
+   *
+   * @return CRM_Utils_Check_Security
+   */
+  static function &singleton() {
+    if (!isset(self::$_singleton)) {
+      self::$_singleton = new CRM_Utils_Check_Security();
+    }
+    return self::$_singleton;
+  }
+
+  /**
+   * Run some sanity checks.
+   *
+   * This could become a hook so that CiviCRM can run both built-in
+   * configuration & sanity checks, and modules/extensions can add
+   * their own checks.
+   *
+   * We might even expose the results of these checks on the Wordpress
+   * plugin status page or the Drupal admin/reports/status path.
+   *
+   * @see Drupal's hook_requirements() -
+   * https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_requirements
+   */
+  public function allChecks() {
+    if (CRM_Core_Permission::check('administer CiviCRM')) {
+      CRM_Utils_Check_Security::singleton()->CheckLogFileIsNotAccessible();
+      CRM_Utils_Check_Security::singleton()->CheckUploadsAreNotAccessible();
+      CRM_Utils_Check_Security::singleton()->CheckDirectoriesAreNotBrowseable();
+    }
+  }
+
+  /**
+   * Check if our logfile is directly accessible.
+   *
+   * Per CiviCRM default the logfile sits in a folder which is
+   * web-accessible, and is protected by a default .htaccess
+   * configuration. If server config causes the .htaccess not to
+   * function as intended, there may be information disclosure.
+   *
+   * The debug log may be jam-packed with sensitive data, we don't
+   * want that.
+   *
+   * Being able to be retrieved directly doesn't mean the logfile
+   * is browseable or visible to search engines; it means it can be
+   * requested directly.
+   *
+   * @see CRM-14091
+   */
+  public function CheckLogFileIsNotAccessible() {
+    $config = CRM_Core_Config::singleton();
+
+    $log = CRM_Core_Error::createDebugLogger();
+    $log_filename = $log->_filename;
+
+    // Hazard a guess at the URL of the logfile, based on common
+    // CiviCRM layouts.
+    switch ($config->userFramework) {
+      // If other frameworks lay out differently, add them here.
+
+      // Drupal style - look for '/files/' and stitch the known paths
+      // (based on CIVICRM_TEMPLATE_COMPILEDIR and $config->uploadDir)
+      // together.
+      case 'Drupal':
+      case 'Drupal6':
+      default:
+        if ($upload_url = explode('/files/', $config->imageUploadURL)) {
+          $url[] = $upload_url[0];
+          if ($log_path = explode('/files/', $log_filename)) {
+            $url[] = $log_path[1];
+            $log_url = implode('/files/', $url);
+            // Fake a log being internet-accessible.
+            // $log_url = 'https://gist.github.com/xurizaemon/2141ee4e042c273c8979/raw/3eda5da63b114e206c2516569f88a45305cb1469/CiviCRM.aabbccdd.log';
+            $docs_url = 'http://wiki.civicrm.org/confluence/display/CRMDOC/Security/LogNotAccessible';
+            if ($log = @file_get_contents($log_url)) {
+              $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
+                . '<br />' .
+                '<a href="%2">Read more about this warning</a>';
+              $msg = ts($msg, array(1 => $log_url, 2 => $docs_url));
+              CRM_Core_Session::setStatus($msg, ts('Security Warning'));
+            }
+          }
+        }
+    }
+  }
+
+  /**
+   * Check if our uploads directory has accessible files.
+   *
+   * We'll test a handful of files randomly. Hazard a guess at the URL
+   * of the uploads dir, based on common CiviCRM layouts. Try and
+   * request the files, and if any are successfully retrieved, warn.
+   *
+   * Being retrievable doesn't mean the files are browseable or visible
+   * to search engines; it only means they can be requested directly.
+   *
+   * @see CRM-14091
+   */
+  public function CheckUploadsAreNotAccessible() {
+    $config = CRM_Core_Config::singleton();
+    // @TODO: Test with WordPress, Joomla.
+    switch ($config->userFramework) {
+      // Drupal style - look for '/files/' and stitch the known paths
+      // (based on CIVICRM_TEMPLATE_COMPILEDIR and $config->uploadDir)
+      // together.
+      case 'Drupal':
+      case 'Drupal6':
+      default:
+        if ($upload_url = explode('/files/', $config->imageUploadURL)) {
+          if ($files = glob($config->uploadDir . '/*')) {
+            for ($i=0; $i<3; $i++) {
+              $f = array_rand($files);
+              if ($file_path = explode('/files/', $files[$f])) {
+                $url = implode('/files/', array($upload_url[0], $file_path[1]));
+                if ($file = @file_get_contents($url)) {
+                  $msg = 'Files in the upload directory should not be downloadable.'
+                    . '<br />' .
+                    '<a href="%2">Read more about this warning</a>';
+                  $docs_url = 'http://wiki.civicrm.org/confluence/display/CRMDOC/Security/UploadDirNotAccessible';
+                  $msg = ts($msg, array(1 => $docs_url));
+                  CRM_Core_Session::setStatus($msg, ts('Security Warning'));
+                }
+              }
+            }
+          }
+        }
+    }
+  }
+
+  /**
+   * Check if our uploads or ConfigAndLog directories have browseable
+   * listings.
+   *
+   * Retrieve a listing of files from the local filesystem, and the
+   * corresponding path via HTTP. Then check and see if the local
+   * files are represented in the HTTP result; if so then warn. This
+   * MAY trigger false positives (if you have files named 'a', 'e'
+   * we'll probably match that).
+   *
+   * @see CRM-14091
+   */
+  public function CheckDirectoriesAreNotBrowseable() {
+    $config = CRM_Core_Config::singleton();
+    $log = CRM_Core_Error::createDebugLogger();
+    $log_name = $log->_filename;
+
+    // @TODO: Test with WordPress, Joomla.
+    switch ($config->userFramework) {
+      // Drupal style - look for '/files/' and stitch the known paths
+      // (based on CIVICRM_TEMPLATE_COMPILEDIR and URL settings)
+      // together.
+      case 'Drupal':
+      case 'Drupal6':
+      default:
+        $paths = array(
+          $config->uploadDir,
+          dirname($log_name),
+        );
+        if ($upload_url = explode('/files/', $config->imageUploadURL)) {
+          if ($files = glob($config->uploadDir . '/*')) {
+            foreach ($paths as $path) {
+              if ($dir_path = explode('/files/', $path)) {
+                $url = implode('/files/', array($upload_url[0], $dir_path[1]));
+                if ($files = glob($path . '/*')) {
+                  if ($listing = @file_get_contents($url)) {
+                    foreach ($files as $file) {
+                      if (stristr($listing, $file)) {
+                        $msg = 'Directory <a href="%1">%2</a> may be browseable via the web.'
+                          . '<br />' .
+                          '<a href="%3">Read more about this warning</a>';
+                        $docs_url = 'http://wiki.civicrm.org/confluence/display/CRMDOC/Security/UploadDirNotAccessible';
+                        $msg = ts($msg, array(1 => $log_url, 2 => $path, 3 => $docs_url));
+                        CRM_Core_Session::setStatus($msg, ts('Security Warning'));
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    }
+  }
+
+}