3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
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-2015
35 class CRM_Utils_Check_Security
{
38 * CMS have a different pattern to their default file path and URL.
40 * @TODO This function might be better shared in CRM_Utils_Check
41 * class, but that class doesn't yet exist.
43 public function getFilePathMarker() {
44 $config = CRM_Core_Config
::singleton();
45 switch ($config->userFramework
) {
55 * Run some sanity checks.
57 * @return array<CRM_Utils_Check_Message>
59 public function checkAll() {
60 $messages = array_merge(
61 $this->checkCxnOverrides(),
62 $this->checkLogFileIsNotAccessible(),
63 $this->checkUploadsAreNotAccessible(),
64 $this->checkDirectoriesAreNotBrowseable(),
65 $this->checkFilesAreNotPresent()
71 * Check if our logfile is directly accessible.
73 * Per CiviCRM default the logfile sits in a folder which is
74 * web-accessible, and is protected by a default .htaccess
75 * configuration. If server config causes the .htaccess not to
76 * function as intended, there may be information disclosure.
78 * The debug log may be jam-packed with sensitive data, we don't
81 * Being able to be retrieved directly doesn't mean the logfile
82 * is browseable or visible to search engines; it means it can be
89 public function checkLogFileIsNotAccessible() {
92 $config = CRM_Core_Config
::singleton();
94 $log = CRM_Core_Error
::createDebugLogger();
95 $log_filename = str_replace('\\', '/', $log->_filename
);
97 $filePathMarker = $this->getFilePathMarker();
99 // Hazard a guess at the URL of the logfile, based on common
101 if ($upload_url = explode($filePathMarker, $config->imageUploadURL
)) {
102 $url[] = $upload_url[0];
103 if ($log_path = explode($filePathMarker, $log_filename)) {
104 $url[] = $log_path[1];
105 $log_url = implode($filePathMarker, $url);
106 $headers = @get_headers
($log_url);
107 if (stripos($headers[0], '200')) {
108 $docs_url = $this->createDocUrl('checkLogFileIsNotAccessible');
109 $msg = 'The <a href="%1">CiviCRM debug log</a> should not be downloadable.'
111 '<a href="%2">Read more about this warning</a>';
112 $messages[] = new CRM_Utils_Check_Message(
113 'checkLogFileIsNotAccessible',
114 ts($msg, array(1 => $log_url, 2 => $docs_url)),
115 ts('Security Warning')
125 * Check if our uploads directory has accessible files.
127 * We'll test a handful of files randomly. Hazard a guess at the URL
128 * of the uploads dir, based on common CiviCRM layouts. Try and
129 * request the files, and if any are successfully retrieved, warn.
131 * Being retrievable doesn't mean the files are browseable or visible
132 * to search engines; it only means they can be requested directly.
138 * @TODO: Test with WordPress, Joomla.
140 public function checkUploadsAreNotAccessible() {
143 $config = CRM_Core_Config
::singleton();
144 $privateDirs = array(
146 $config->customFileUploadDir
,
149 foreach ($privateDirs as $privateDir) {
150 $heuristicUrl = $this->guessUrl($privateDir);
151 if ($this->isDirAccessible($privateDir, $heuristicUrl)) {
152 $messages[] = new CRM_Utils_Check_Message(
153 'checkUploadsAreNotAccessible',
154 ts('Files in the data directory (<a href="%3">%2</a>) should not be downloadable.'
156 . '<a href="%1">Read more about this warning</a>',
158 1 => $this->createDocUrl('checkUploadsAreNotAccessible'),
162 ts('Security Warning')
171 * Check if our uploads or ConfigAndLog directories have browseable
174 * Retrieve a listing of files from the local filesystem, and the
175 * corresponding path via HTTP. Then check and see if the local
176 * files are represented in the HTTP result; if so then warn. This
177 * MAY trigger false positives (if you have files named 'a', 'e'
178 * we'll probably match that).
184 * @TODO: Test with WordPress, Joomla.
186 public function checkDirectoriesAreNotBrowseable() {
188 $config = CRM_Core_Config
::singleton();
190 $config->imageUploadDir
=> $config->imageUploadURL
,
193 // Setup index.html files to prevent browsing
194 foreach ($publicDirs as $publicDir => $publicUrl) {
195 CRM_Utils_File
::restrictBrowsing($publicDir);
198 // Test that $publicDir is not browsable
199 foreach ($publicDirs as $publicDir => $publicUrl) {
200 if ($this->isBrowsable($publicDir, $publicUrl)) {
201 $msg = 'Directory <a href="%1">%2</a> should not be browseable via the web.'
203 '<a href="%3">Read more about this warning</a>';
204 $docs_url = $this->createDocUrl('checkDirectoriesAreNotBrowseable');
205 $messages[] = new CRM_Utils_Check_Message(
206 'checkDirectoriesAreNotBrowseable',
207 ts($msg, array(1 => $publicDir, 2 => $publicDir, 3 => $docs_url)),
208 ts('Security Warning')
218 * Check that some files are not present.
220 * These files have generally been deleted but Civi source tree but could be
221 * left online if one does a faulty upgrade.
223 * @return array of messages
225 public function checkFilesAreNotPresent() {
226 global $civicrm_root;
230 "{$civicrm_root}/packages/dompdf/dompdf.php", // CRM-16005, upgraded from Civi <= 4.5.6
231 "{$civicrm_root}/packages/vendor/dompdf/dompdf/dompdf.php", // CRM-16005, Civi >= 4.5.7
232 "{$civicrm_root}/vendor/dompdf/dompdf/dompdf.php", // CRM-16005, Civi >= 4.6.0
234 foreach ($files as $file) {
235 if (file_exists($file)) {
236 $messages[] = new CRM_Utils_Check_Message(
237 'checkFilesAreNotPresent',
238 ts('File \'%1\' presents a security risk and should be deleted.', array(1 => $file)),
239 ts('Security Warning')
247 * Check that the sysadmin has not modified the Cxn
250 public function checkCxnOverrides() {
252 if (defined('CIVICRM_CXN_CA') && CIVICRM_CXN_CA
!== 'CiviRootCA') {
253 $list[] = 'CIVICRM_CXN_CA';
255 if (defined('CIVICRM_CXN_APPS_URL') && CIVICRM_CXN_APPS_URL
!== \Civi\Cxn\Rpc\Constants
::OFFICIAL_APPMETAS_URL
) {
256 $list[] = 'CIVICRM_CXN_APPS_URL';
262 $messages[] = new CRM_Utils_Check_Message(
264 ts('The system administrator has disabled security settings (%1). Connections to remote applications are insecure.', array(
265 1 => implode(', ', $list),
267 ts('Security Warning')
275 * Determine whether $url is a public, browsable listing for $dir
283 public function isBrowsable($dir, $url) {
284 if (empty($dir) ||
empty($url) ||
!is_dir($dir)) {
289 $file = 'delete-this-' . CRM_Utils_String
::createRandom(10, CRM_Utils_String
::ALPHANUMERIC
);
291 // this could be a new system with no uploads (yet) -- so we'll make a file
292 file_put_contents("$dir/$file", "delete me");
293 $content = @file_get_contents
("$url");
294 if (stristr($content, $file)) {
297 unlink("$dir/$file");
303 * Determine whether $url is a public version of $dir in which files
304 * are remotely accessible.
312 public function isDirAccessible($dir, $url) {
313 $dir = rtrim($dir, '/');
314 $url = rtrim($url, '/');
315 if (empty($dir) ||
empty($url) ||
!is_dir($dir)) {
320 $file = 'delete-this-' . CRM_Utils_String
::createRandom(10, CRM_Utils_String
::ALPHANUMERIC
);
322 // this could be a new system with no uploads (yet) -- so we'll make a file
323 file_put_contents("$dir/$file", "delete me");
325 $headers = @get_headers
("$url/$file");
326 if (stripos($headers[0], '200')) {
327 $content = @file_get_contents
("$url/$file");
328 if (preg_match('/delete me/', $content)) {
333 unlink("$dir/$file");
343 public function createDocUrl($topic) {
344 return CRM_Utils_System
::getWikiBaseURL() . $topic;
348 * Make a guess about the URL that corresponds to $targetDir.
350 * @param string $targetDir
351 * Local path to a directory.
353 * a guessed URL for $realDir
355 public function guessUrl($targetDir) {
356 $filePathMarker = $this->getFilePathMarker();
357 $config = CRM_Core_Config
::singleton();
359 list ($heuristicBaseUrl, $ignore) = explode($filePathMarker, $config->imageUploadURL
);
360 list ($ignore, $heuristicSuffix) = explode($filePathMarker, str_replace('\\', '/', $targetDir));
361 return $heuristicBaseUrl . $filePathMarker . $heuristicSuffix;