2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
39 private $manifestMap = array(
40 'pre_execute'=>'pre_execute',
41 'install_mkdirs'=>'mkdir',
42 'install_copy'=>'copy',
43 'install_images'=>'image_dir',
44 'install_menus'=>'menu',
45 'install_userpage'=>'user_page',
46 'install_dashlets'=>'dashlets',
47 'install_administration'=>'administration',
48 'install_connectors'=>'connectors',
49 'install_vardefs'=>'vardefs',
50 'install_layoutdefs'=>'layoutdefs',
51 'install_layoutfields'=>'layoutfields',
52 'install_relationships'=>'relationships',
53 'install_languages'=>'language',
54 'install_logichooks'=>'logic_hooks',
55 'post_execute'=>'post_execute',
63 private $config = array();
66 private $blackListExempt = array();
67 private $classBlackListExempt = array();
69 // Bug 56717 - adding hbs extension to the whitelist - rgonzalez
70 private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml', 'hbs');
71 private $classBlackList = array(
72 // Class names specified here must be in lowercase as the implementation
73 // of the tokenizer converts all tokens to lowercase.
76 'reflectionzendextension',
77 'reflectionextension',
79 'reflectionfunctionabstract',
82 'reflectionparameter',
85 'reflectionexception',
93 private $blackList = array(
142 'realpath_cache_get',
143 'realpath_cache_size',
188 'call_user_func_array',
192 //mutliple files per function call
198 'move_uploaded_file',
201 'create_cache_directory',
203 'write_array_to_file',
204 'write_encoded_file',
205 'create_custom_directory',
210 'sugar_file_put_contents',
215 // Functions that have callbacks can circumvent our security measures.
216 // List retrieved through PHP's XML documentation, and running the
217 // following script in the reference directory:
219 // grep -R callable . | grep -v \.svn | grep methodparam | cut -d: -f1 | sort -u | cut -d"." -f2 | sed 's/\-/\_/g' | cut -d"/" -f4
224 // PHP internal - arrays
228 'array_intersect_uassoc',
229 'array_intersect_ukey',
233 'array_udiff_uassoc',
235 'array_uintersect_assoc',
236 'array_uintersect_uassoc',
238 'array_walk_recursive',
244 // EIO functions that accept callbacks.
278 'eio_sync_file_range',
286 // PHP internal - error functions
288 'set_exception_handler',
290 // Forms Data Format functions
293 // PHP internal - function handling
294 'call_user_func_array',
296 'forward_static_call_array',
297 'forward_static_call',
298 'register_shutdown_function',
299 'register_tick_function',
303 'setcompletecallback',
305 'setexceptioncallback',
308 'setwarningcallback',
309 'setworkloadcallback',
312 // Firebird/InterBase
313 'ibase_set_event_handler',
316 'ldap_set_rebind_proc',
319 'libxml_set_external_entity_loader',
321 // Mailparse functions
322 'mailparse_msg_extract_part_file',
323 'mailparse_msg_extract_part',
324 'mailparse_msg_extract_whole_part_file',
326 // Memcache(d) functions
335 'set_local_infile_handler',
337 // PHP internal - network functions
338 'header_register_callback',
341 'newt_entry_set_filter',
342 'newt_set_suspend_callback',
346 'timestampnoncehandler',
349 // PHP internal - output control
352 // PHP internal - PCNTL
355 // PHP internal - PCRE
356 'preg_replace_callback',
359 'sqlitecreateaggregate',
360 'sqlitecreatefunction',
361 'sqlite_create_aggregate',
362 'sqlite_create_function',
368 'readline_callback_handler_install',
369 'readline_completion_function',
371 // PHP internal - session handling
372 'session_set_save_handler',
374 // PHP internal - SPL
377 'spl_autoload_register',
380 'sybase_set_message_handler',
382 // PHP internal - variable handling
386 'xml_set_character_data_handler',
387 'xml_set_default_handler',
388 'xml_set_element_handler',
389 'xml_set_end_namespace_decl_handler',
390 'xml_set_external_entity_ref_handler',
391 'xml_set_notation_decl_handler',
392 'xml_set_processing_instruction_handler',
393 'xml_set_start_namespace_decl_handler',
394 'xml_set_unparsed_entity_decl_handler',
400 private $methodsBlackList = array('setlevel', 'put' => array('sugarautoloader'), 'unlink' => array('sugarautoloader'));
402 public function printToWiki(){
403 echo "'''Default Extensions'''<br>";
404 foreach($this->validExt as $b){
405 echo '#' . $b . '<br>';
408 echo "'''Default Black Listed Functions'''<br>";
409 foreach($this->blackList as $b){
410 echo '#' . $b . '<br>';
416 public function __construct()
419 'blackListExempt' => 'MODULE_INSTALLER_PACKAGE_SCAN_BLACK_LIST_EXEMPT',
420 'blackList' => 'MODULE_INSTALLER_PACKAGE_SCAN_BLACK_LIST',
421 'classBlackListExempt' => 'MODULE_INSTALLER_PACKAGE_SCAN_CLASS_BLACK_LIST_EXEMPT',
422 'classBlackList' => 'MODULE_INSTALLER_PACKAGE_SCAN_CLASS_BLACK_LIST',
423 'validExt' => 'MODULE_INSTALLER_PACKAGE_SCAN_VALID_EXT',
424 'methodsBlackList' => 'MODULE_INSTALLER_PACKAGE_SCAN_METHOD_LIST',
427 $disableConfigOverride = defined('MODULE_INSTALLER_DISABLE_CONFIG_OVERRIDE')
428 && MODULE_INSTALLER_DISABLE_CONFIG_OVERRIDE;
430 $disableDefineOverride = defined('MODULE_INSTALLER_DISABLE_DEFINE_OVERRIDE')
431 && MODULE_INSTALLER_DISABLE_DEFINE_OVERRIDE;
433 if (!$disableConfigOverride && !empty($GLOBALS['sugar_config']['moduleInstaller'])) {
434 $this->config = $GLOBALS['sugar_config']['moduleInstaller'];
437 foreach ($params as $param => $constName) {
439 if (!$disableConfigOverride && isset($this->config[$param]) && is_array($this->config[$param])) {
440 $this->{$param} = array_merge($this->{$param}, $this->config[$param]);
443 if (!$disableDefineOverride && defined($constName)) {
444 $value = constant($constName);
445 $value = explode(',', $value);
446 $value = array_map('trim', $value);
447 $value = array_filter($value, 'strlen');
448 $this->{$param} = array_merge($this->{$param}, $value);
453 private $issues = array();
454 private $pathToModule = '';
457 *returns a list of issues
459 public function getIssues(){
460 return $this->issues;
464 *returns true or false if any issues were found
466 public function hasIssues(){
467 return !empty($this->issues);
471 *Ensures that a file has a valid extension
473 public function isValidExtension($file)
475 $file = strtolower($file);
476 $pi = pathinfo($file);
478 //make sure they don't override the files.md5
479 if(empty($pi['extension']) || $pi['basename'] == 'files.md5') {
482 return in_array($pi['extension'], $this->validExt);
486 public function isConfigFile($file)
488 $real = realpath($file);
489 if($real == realpath("config.php")) {
492 if(file_exists("config_override.php") && $real == realpath("config_override.php")) {
499 *Scans a directory and calls on scan file for each file
501 public function scanDir($path){
502 static $startPath = '';
503 if(empty($startPath))$startPath = $path;
504 if(!is_dir($path))return false;
506 while($e = $d->read()){
507 $next = $path . '/' . $e;
509 if(substr($e, 0, 1) == '.')continue;
510 $this->scanDir($next);
512 $issues = $this->scanFile($next);
521 * Check if the file contents looks like PHP
522 * @param string $contents File contents
525 public function isPHPFile($contents)
527 if(stripos($contents, '<?php') !== false) return true;
528 for($tag=0;($tag = stripos($contents, '<?', $tag)) !== false;$tag++) {
529 if(strncasecmp(substr($contents, $tag, 13), '<?xml version', 13) == 0) {
530 // <?xml version is OK, skip it
534 // found <?, it's PHP
541 * Given a file it will open it's contents and check if it is a PHP file (not safe to just rely on extensions) if it finds <?php tags it will use the tokenizer to scan the file
542 * $var() and ` are always prevented then whatever is in the blacklist.
543 * It will also ensure that all files are of valid extension types
546 public function scanFile($file){
548 if(!$this->isValidExtension($file)){
549 $issues[] = translate('ML_INVALID_EXT');
550 $this->issues['file'][$file] = $issues;
553 if($this->isConfigFile($file)){
554 $issues[] = translate('ML_OVERRIDE_CORE_FILES');
555 $this->issues['file'][$file] = $issues;
558 $contents = file_get_contents($file);
559 if(!$this->isPHPFile($contents)) return $issues;
560 $tokens = @token_get_all($contents);
561 $checkFunction = false;
564 foreach($tokens as $index=>$token){
565 if(is_string($token[0])){
568 $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
570 if($checkFunction)$issues[] = $possibleIssue;
573 $checkFunction = false;
576 $token['_msi'] = token_name($token[0]);
578 case T_WHITESPACE: continue;
580 if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
581 $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
584 $token[1] = strtolower($token[1]);
585 if($lastToken !== false && $lastToken[0] == T_NEW) {
586 if(!in_array($token[1], $this->classBlackList))break;
587 if(in_array($token[1], $this->classBlackListExempt))break;
588 } elseif ($token[0] == T_DOUBLE_COLON) {
589 if(!in_array($lastToken[1], $this->classBlackList))break;
590 if(in_array($lastToken[1], $this->classBlackListExempt))break;
592 //if nothing else fit, lets check the last token to see if this is a possible method call
593 if ($lastToken !== false &&
594 ($lastToken[0] == T_OBJECT_OPERATOR || $lastToken[0] == T_DOUBLE_COLON))
596 // check static blacklist for methods
597 if(!empty($this->methodsBlackList[$token[1]])) {
598 if($this->methodsBlackList[$token[1]] == '*') {
599 $issues[]= translate('ML_INVALID_METHOD') . ' ' .$token[1]. '()';
602 if($lastToken[0] == T_DOUBLE_COLON && $index > 2 && $tokens[$index-2][0] == T_STRING) {
603 $classname = strtolower($tokens[$index-2][1]);
604 if(in_array($classname, $this->methodsBlackList[$token[1]])) {
605 $issues[]= translate('ML_INVALID_METHOD') . ' ' .$classname . '::' . $token[1]. '()';
611 //this is a method call, check the black list
612 if(in_array($token[1], $this->methodsBlackList)){
613 $issues[]= translate('ML_INVALID_METHOD') . ' ' .$token[1]. '()';
619 if(!in_array($token[1], $this->blackList))break;
620 if(in_array($token[1], $this->blackListExempt))break;
624 $checkFunction = true;
625 $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' . $token[1] . '()';
629 $checkFunction = false;
633 if ($token[0] != T_WHITESPACE)
641 $this->issues['file'][$file] = $issues;
649 * checks files.md5 file to see if the file is from sugar
650 * ONLY WORKS ON FILES
652 public function sugarFileExists($path){
653 static $md5 = array();
654 if(empty($md5) && file_exists('files.md5'))
656 include('files.md5');
659 if(isset($md5['./' . $path]))return true;
666 *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
667 *if $GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy'] is set to false or not set it will call on scanCopy to ensure that it is not overriding files
669 public function scanManifest($manifestPath){
671 if(!file_exists($manifestPath)){
672 $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
675 $fileIssues = $this->scanFile($manifestPath);
676 //if the manifest contains malicious code do not open it
677 if(!empty($fileIssues)){
681 list($manifest, $installdefs) = MSLoadManifest($manifestPath);
682 $fileIssues = $this->checkConfig($manifestPath);
683 if(!empty($fileIssues)){
687 //scan for disabled actions
688 if(isset($this->config['disableActions'])){
689 foreach($this->config['disableActions'] as $action){
690 if(isset($installdefs[$this->manifestMap[$action]])){
691 $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
696 //now lets scan for files that will override our files
697 if(empty($this->config['disableRestrictedCopy']) && isset($installdefs['copy'])){
698 foreach($installdefs['copy'] as $copy){
699 $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
701 if(substr_count($from, '..')){
702 $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
704 if(substr_count($to, '..')){
705 $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
707 while(substr_count($from, '//')){
708 $from = str_replace('//', '/', $from);
710 while(substr_count($to, '//')){
711 $to = str_replace('//', '/', $to);
713 $this->scanCopy($from, $to);
717 $this->issues['manifest'][$manifestPath] = $issues;
727 * Takes in where the file will is specified to be copied from and to
728 * and ensures that there is no official sugar file there. If the file exists it will check
729 * against the MD5 file list to see if Sugar Created the file
732 function scanCopy($from, $to){
733 //if the file doesn't exist for the $to then it is not overriding anything
734 if(!file_exists($to))return;
735 //if $to is a dir and $from is a file then make $to a full file path as well
736 if(is_dir($to) && is_file($from)){
737 if(substr($to,-1) === '/'){
738 $to = substr($to, 0 , strlen($to) - 1);
740 $to .= '/'. basename($from);
742 //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
743 if(is_file($to) && $this->sugarFileExists($to)){
744 $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
749 while($e = $d->read()){
750 if($e == '.' || $e == '..')continue;
751 $this->scanCopy($from .'/'. $e, $to .'/' . $e);
763 *Main external function that takes in a path to a package and then scans
764 *that package's manifest for disabled actions and then it scans the PHP files
765 *for restricted function calls
768 public function scanPackage($path){
769 $this->pathToModule = $path;
770 $this->scanManifest($path . '/manifest.php');
771 if(empty($this->config['disableFileScan'])){
772 $this->scanDir($path);
777 *This function will take all issues of the current instance and print them to the screen
779 public function displayIssues($package='Package'){
780 echo '<h2>'.str_replace('{PACKAGE}' , $package ,translate('ML_PACKAGE_SCANNING')). '</h2><BR><h2 class="error">' . translate('ML_INSTALLATION_FAILED') . '</h2><br><p>' .str_replace('{PACKAGE}' , $package ,translate('ML_PACKAGE_NOT_CONFIRM')). '</p><ul><li>'. translate('ML_OBTAIN_NEW_PACKAGE') . '<li>' . translate('ML_RELAX_LOCAL').
781 '</ul></p><br>' . translate('ML_SUGAR_LOADING_POLICY') . ' <a href=" http://kb.sugarcrm.com/custom/module-loader-restrictions-for-sugar-open-cloud/">' . translate('ML_SUGAR_KB') . '</a>.'.
782 '<br>' . translate('ML_AVAIL_RESTRICTION'). ' <a href=" http://developers.sugarcrm.com/wordpress/2009/08/14/module-loader-restrictions/">' . translate('ML_SUGAR_DZ') . '</a>.<br><br>';
785 foreach($this->issues as $type=>$issues){
786 echo '<div class="error"><h2>'. ucfirst($type) .' ' . translate('ML_ISSUES') . '</h2> </div>';
787 echo '<div id="details' . $type . '" >';
788 foreach($issues as $file=>$issue){
789 $file = str_replace($this->pathToModule . '/', '', $file);
790 echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
791 if(is_array($issue)){
792 foreach($issue as $i){
803 echo "<br><input class='button' onclick='document.location.href=\"index.php?module=Administration&action=UpgradeWizard&view=module\"' type='button' value=\"" . translate('LBL_UW_BTN_BACK_TO_MOD_LOADER') . "\" />";
808 * Lock config settings
810 public function lockConfig()
812 if(empty($this->config_hash)) {
813 $this->config_hash = md5(serialize($GLOBALS['sugar_config']));
818 * Check if config was modified. Return
819 * @param string $file
820 * @return array Errors if something wrong, false if no problems
822 public function checkConfig($file)
824 $config_hash_after = md5(serialize($GLOBALS['sugar_config']));
825 if($config_hash_after != $this->config_hash) {
826 $this->issues['file'][$file] = array(translate('ML_CONFIG_OVERRIDE'));
827 return $this->issues;
836 * Outside of the class to isolate the context
837 * @param string $manifest_file
840 function MSLoadManifest($manifest_file)
842 include( $manifest_file );
843 return array($manifest, $installdefs);