]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - ModuleInstall/ModuleScanner.php
Release 6.5.0
[Github/sugarcrm.git] / ModuleInstall / ModuleScanner.php
1 <?php
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-2012 SugarCRM Inc.
6  * 
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.
13  * 
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
17  * details.
18  * 
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
22  * 02110-1301 USA.
23  * 
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.
26  * 
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.
30  * 
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  ********************************************************************************/
37
38 class ModuleScanner{
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',
56
57         );
58
59         private $blackListExempt = array();
60         private $classBlackListExempt = array();
61
62         private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
63         private $classBlackList = array(
64         // Class names specified here must be in lowercase as the implementation
65         // of the tokenizer converts all tokens to lowercase.
66         'reflection',
67         'reflectionclass',
68         'reflectionzendextension',
69         'reflectionextension',
70         'reflectionfunction',
71         'reflectionfunctionabstract',
72         'reflectionmethod',
73         'reflectionobject',
74         'reflectionparameter',
75         'reflectionproperty',
76         'reflector',
77         'reflectionexception',
78         'lua',
79     );
80         private $blackList = array(
81     'popen',
82     'proc_open',
83     'escapeshellarg',
84     'escapeshellcmd',
85     'proc_close',
86     'proc_get_status',
87     'proc_nice',
88     'basename',
89         'passthru',
90     'clearstatcache',
91     'delete',
92     'dirname',
93     'disk_free_space',
94     'disk_total_space',
95     'diskfreespace',
96     'fclose',
97     'feof',
98     'fflush',
99     'fgetc',
100     'fgetcsv',
101     'fgets',
102     'fgetss',
103     'file_exists',
104     'file_get_contents',
105     'filesize',
106     'filetype',
107     'flock',
108     'fnmatch',
109     'fpassthru',
110     'fputcsv',
111     'fputs',
112     'fread',
113     'fscanf',
114     'fseek',
115     'fstat',
116     'ftell',
117     'ftruncate',
118     'fwrite',
119     'glob',
120     'is_dir',
121     'is_file',
122     'is_link',
123     'is_readable',
124     'is_uploaded_file',
125     'parse_ini_string',
126     'pathinfo',
127     'pclose',
128     'readfile',
129     'readlink',
130     'realpath_cache_get',
131     'realpath_cache_size',
132     'realpath',
133     'rewind',
134     'set_file_buffer',
135     'tmpfile',
136     'umask',
137         'eval',
138         'exec',
139         'system',
140         'shell_exec',
141         'passthru',
142         'chgrp',
143         'chmod',
144         'chwown',
145         'file_put_contents',
146         'file',
147         'fileatime',
148         'filectime',
149         'filegroup',
150         'fileinode',
151         'filemtime',
152         'fileowner',
153         'fileperms',
154         'fopen',
155         'is_executable',
156         'is_writable',
157         'is_writeable',
158         'lchgrp',
159         'lchown',
160         'linkinfo',
161         'lstat',
162         'mkdir',
163         'parse_ini_file',
164         'rmdir',
165         'stat',
166         'tempnam',
167         'touch',
168         'unlink',
169         'getimagesize',
170         'call_user_func',
171         'call_user_func_array',
172         'create_function',
173
174
175         //mutliple files per function call
176         'copy',
177         'link',
178         'rename',
179         'symlink',
180         'move_uploaded_file',
181         'chdir',
182         'chroot',
183         'create_cache_directory',
184         'mk_temp_dir',
185         'write_array_to_file',
186         'write_encoded_file',
187         'create_custom_directory',
188         'sugar_rename',
189         'sugar_chown',
190         'sugar_fopen',
191         'sugar_mkdir',
192         'sugar_file_put_contents',
193         'sugar_chgrp',
194         'sugar_chmod',
195         'sugar_touch',
196
197         // Functions that have callbacks can circumvent our security measures.
198         // List retrieved through PHP's XML documentation, and running the
199         // following script in the reference directory:
200
201         // grep -R callable . | grep -v \.svn | grep methodparam | cut -d: -f1 | sort -u | cut -d"." -f2 | sed 's/\-/\_/g' | cut -d"/" -f4
202
203         // AMQPQueue
204         'consume',
205
206         // PHP internal - arrays
207         'array_diff_uassoc',
208         'array_diff_ukey',
209         'array_filter',
210         'array_intersect_uassoc',
211         'array_intersect_ukey',
212         'array_map',
213         'array_reduce',
214         'array_udiff_assoc',
215         'array_udiff_uassoc',
216         'array_udiff',
217         'array_uintersect_assoc',
218         'array_uintersect_uassoc',
219         'array_uintersect',
220         'array_walk_recursive',
221         'array_walk',
222         'uasort',
223         'uksort',
224         'usort',
225
226         // EIO functions that accept callbacks.
227         'eio_busy',
228         'eio_chmod',
229         'eio_chown',
230         'eio_close',
231         'eio_custom',
232         'eio_dup2',
233         'eio_fallocate',
234         'eio_fchmod',
235         'eio_fchown',
236         'eio_fdatasync',
237         'eio_fstat',
238         'eio_fstatvfs',
239         'eio_fsync',
240         'eio_ftruncate',
241         'eio_futime',
242         'eio_grp',
243         'eio_link',
244         'eio_lstat',
245         'eio_mkdir',
246         'eio_mknod',
247         'eio_nop',
248         'eio_open',
249         'eio_read',
250         'eio_readahead',
251         'eio_readdir',
252         'eio_readlink',
253         'eio_realpath',
254         'eio_rename',
255         'eio_rmdir',
256         'eio_sendfile',
257         'eio_stat',
258         'eio_statvfs',
259         'eio_symlink',
260         'eio_sync_file_range',
261         'eio_sync',
262         'eio_syncfs',
263         'eio_truncate',
264         'eio_unlink',
265         'eio_utime',
266         'eio_write',
267
268         // PHP internal - error functions
269         'set_error_handler',
270         'set_exception_handler',
271
272         // Forms Data Format functions
273         'fdf_enum_values',
274
275         // PHP internal - function handling
276         'call_user_func_array',
277         'call_user_func',
278         'forward_static_call_array',
279         'forward_static_call',
280         'register_shutdown_function',
281         'register_tick_function',
282
283         // Gearman
284         'setclientcallback',
285         'setcompletecallback',
286         'setdatacallback',
287         'setexceptioncallback',
288         'setfailcallback',
289         'setstatuscallback',
290         'setwarningcallback',
291         'setworkloadcallback',
292         'addfunction',
293
294         // Firebird/InterBase
295         'ibase_set_event_handler',
296
297         // LDAP
298         'ldap_set_rebind_proc',
299
300         // LibXML
301         'libxml_set_external_entity_loader',
302
303         // Mailparse functions
304         'mailparse_msg_extract_part_file',
305         'mailparse_msg_extract_part',
306         'mailparse_msg_extract_whole_part_file',
307
308         // Memcache(d) functions
309         'addserver',
310         'setserverparams',
311         'get',
312         'getbykey',
313         'getdelayed',
314         'getdelayedbykey',
315
316         // MySQLi
317         'set_local_infile_handler',
318
319         // PHP internal - network functions
320         'header_register_callback',
321
322         // Newt
323         'newt_entry_set_filter',
324         'newt_set_suspend_callback',
325
326         // OAuth
327         'consumerhandler',
328         'timestampnoncehandler',
329         'tokenhandler',
330
331         // PHP internal - output control
332         'ob_start',
333
334         // PHP internal - PCNTL
335         'pcntl_signal',
336
337         // PHP internal - PCRE
338         'preg_replace_callback',
339
340         // SQLite
341         'sqlitecreateaggregate',
342         'sqlitecreatefunction',
343         'sqlite_create_aggregate',
344         'sqlite_create_function',
345
346         // RarArchive
347         'open',
348
349         // Readline
350         'readline_callback_handler_install',
351         'readline_completion_function',
352
353         // PHP internal - session handling
354         'session_set_save_handler',
355
356         // PHP internal - SPL
357         'construct',
358         'iterator_apply',
359         'spl_autoload_register',
360
361         // Sybase
362         'sybase_set_message_handler',
363
364         // PHP internal - variable handling
365         'is_callable',
366
367         // XML Parser
368         'xml_set_character_data_handler',
369         'xml_set_default_handler',
370         'xml_set_element_handler',
371         'xml_set_end_namespace_decl_handler',
372         'xml_set_external_entity_ref_handler',
373         'xml_set_notation_decl_handler',
374         'xml_set_processing_instruction_handler',
375         'xml_set_start_namespace_decl_handler',
376         'xml_set_unparsed_entity_decl_handler',
377 );
378
379         public function printToWiki(){
380                 echo "'''Default Extensions'''<br>";
381                 foreach($this->validExt as $b){
382                         echo '#' . $b . '<br>';
383
384                 }
385                 echo "'''Default Black Listed Functions'''<br>";
386                 foreach($this->blackList as $b){
387                         echo '#' . $b . '<br>';
388
389                 }
390
391         }
392
393         public function __construct(){
394                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackListExempt'])){
395                         $this->blackListExempt = array_merge($this->blackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['blackListExempt']);
396                 }
397                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackList'])){
398                         $this->blackList = array_merge($this->blackList, $GLOBALS['sugar_config']['moduleInstaller']['blackList']);
399                 }
400         if(!empty($GLOBALS['sugar_config']['moduleInstaller']['classBlackListExempt'])){
401             $this->classBlackListExempt = array_merge($this->classBlackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['classBlackListExempt']);
402         }
403         if(!empty($GLOBALS['sugar_config']['moduleInstaller']['classBlackList'])){
404             $this->classBlackList = array_merge($this->classBlackList, $GLOBALS['sugar_config']['moduleInstaller']['classBlackList']);
405         }
406           if(!empty($GLOBALS['sugar_config']['moduleInstaller']['validExt'])){
407                         $this->validExt = array_merge($this->validExt, $GLOBALS['sugar_config']['moduleInstaller']['validExt']);
408                 }
409
410         }
411
412         private $issues = array();
413         private $pathToModule = '';
414
415         /**
416          *returns a list of issues
417          */
418         public function getIssues(){
419                 return $this->issues;
420         }
421
422         /**
423          *returns true or false if any issues were found
424          */
425         public function hasIssues(){
426                 return !empty($this->issues);
427         }
428
429         /**
430          *Ensures that a file has a valid extension
431          */
432         private function isValidExtension($file){
433                 $file = strtolower($file);
434
435                 $extPos = strrpos($file, '.');
436                 //make sure they don't override the files.md5
437                 if($extPos === false || $file == 'files.md5')return false;
438                 $ext = substr($file, $extPos + 1);
439                 return in_array($ext, $this->validExt);
440
441         }
442
443         /**
444          *Scans a directory and calls on scan file for each file
445          **/
446         public function scanDir($path){
447                 static $startPath = '';
448                 if(empty($startPath))$startPath = $path;
449                 if(!is_dir($path))return false;
450                 $d = dir($path);
451                 while($e = $d->read()){
452                 $next = $path . '/' . $e;
453                 if(is_dir($next)){
454                         if(substr($e, 0, 1) == '.')continue;
455                         $this->scanDir($next);
456                 }else{
457                         $issues = $this->scanFile($next);
458
459
460                 }
461                 }
462         return true;
463         }
464
465         /**
466          * Check if the file contents looks like PHP
467          * @param string $contents File contents
468          * @return boolean
469          */
470         public function isPHPFile($contents)
471         {
472             if(stripos($contents, '<?php') !== false) return true;
473             for($tag=0;($tag = stripos($contents, '<?', $tag)) !== false;$tag++) {
474             if(strncasecmp(substr($contents, $tag, 13), '<?xml version', 13) == 0) {
475                 // <?xml version is OK, skip it
476                 $tag++;
477                 continue;
478             }
479             // found <?, it's PHP
480             return true;
481             }
482             return false;
483         }
484
485         /**
486          * 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
487          * $var()  and ` are always prevented then whatever is in the blacklist.
488          * It will also ensure that all files are of valid extension types
489          *
490          */
491         public function scanFile($file){
492                 $issues = array();
493                 if(!$this->isValidExtension($file)){
494                         $issues[] = translate('ML_INVALID_EXT');
495                         $this->issues['file'][$file] = $issues;
496                         return $issues;
497                 }
498                 $contents = file_get_contents($file);
499                 if(!$this->isPHPFile($contents)) return $issues;
500                 $tokens = @token_get_all($contents);
501                 $checkFunction = false;
502                 $possibleIssue = '';
503                 $lastToken = false;
504                 foreach($tokens as $index=>$token){
505                         if(is_string($token[0])){
506                                 switch($token[0]){
507                                         case '`':
508                                                 $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
509                                         case '(':
510                                                 if($checkFunction)$issues[] = $possibleIssue;
511                                                 break;
512                                 }
513                                 $checkFunction = false;
514                                 $possibleIssue = '';
515                         }else{
516                                 $token['_msi'] = token_name($token[0]);
517                                 switch($token[0]){
518                                         case T_WHITESPACE: continue;
519                                         case T_EVAL:
520                                                 if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
521                                                 $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
522                                                 break;
523                                         case T_STRING:
524                                                 $token[1] = strtolower($token[1]);
525                                                 if($lastToken !== false && $lastToken[0] == T_NEW) {
526                             if(!in_array($token[1], $this->classBlackList))break;
527                             if(in_array($token[1], $this->classBlackListExempt))break;
528                         } elseif ($token[0] == T_DOUBLE_COLON) {
529                             if(!in_array($lastToken[1], $this->classBlackList))break;
530                             if(in_array($lastToken[1], $this->classBlackListExempt))break;
531                         } else {
532                             if(!in_array($token[1], $this->blackList))break;
533                             if(in_array($token[1], $this->blackListExempt))break;
534
535                             if ($lastToken !== false &&
536                             ($lastToken[0] == T_OBJECT_OPERATOR ||  $lastToken[0] == T_DOUBLE_COLON))
537                             {
538                                 break;
539                             }
540                         }
541                                         case T_VARIABLE:
542                                                 $checkFunction = true;
543                                                 $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' .  $token[1] . '()';
544                                                 break;
545
546                                         default:
547                                                 $checkFunction = false;
548                                                 $possibleIssue = '';
549
550                                 }
551                                 if ($token[0] != T_WHITESPACE)
552                                 {
553                                         $lastToken = $token;
554                                 }
555                         }
556
557                 }
558                 if(!empty($issues)){
559                         $this->issues['file'][$file] = $issues;
560                 }
561
562                 return $issues;
563         }
564
565
566         /*
567          * checks files.md5 file to see if the file is from sugar
568          * ONLY WORKS ON FILES
569          */
570         public function sugarFileExists($path){
571                 static $md5 = array();
572                 if(empty($md5) && file_exists('files.md5'))
573                 {
574                         include('files.md5');
575                         $md5 = $md5_string;
576                 }
577                 if(isset($md5['./' . $path]))return true;
578
579
580         }
581
582
583         /**
584          *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
585          *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
586          */
587         public function scanManifest($manifestPath){
588                 $issues = array();
589                 if(!file_exists($manifestPath)){
590                         $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
591                         return $issues;
592                 }
593                 $fileIssues = $this->scanFile($manifestPath);
594                 //if the manifest contains malicious code do not open it
595                 if(!empty($fileIssues)){
596                         return $fileIssues;
597                 }
598                 include($manifestPath);
599
600
601                 //scan for disabled actions
602                 if(isset($GLOBALS['sugar_config']['moduleInstaller']['disableActions'])){
603                         foreach($GLOBALS['sugar_config']['moduleInstaller']['disableActions'] as $action){
604                                 if(isset($installdefs[$this->manifestMap[$action]])){
605                                         $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
606                                 }
607                         }
608                 }
609
610                 //now lets scan for files that will override our files
611                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy']) && isset($installdefs['copy'])){
612                         foreach($installdefs['copy'] as $copy){
613                                 $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
614                                 $to = $copy['to'];
615                                 if(substr_count($from, '..')){
616                                         $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
617                                 }
618                                 if(substr_count($to, '..')){
619                                         $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
620                                 }
621                                 while(substr_count($from, '//')){
622                                         $from = str_replace('//', '/', $from);
623                                 }
624                                 while(substr_count($to, '//')){
625                                         $to = str_replace('//', '/', $to);
626                                 }
627                                 $this->scanCopy($from, $to);
628                         }
629                 }
630                 if(!empty($issues)){
631                         $this->issues['manifest'][$manifestPath] = $issues;
632                 }
633
634
635
636         }
637
638
639
640         /**
641          * Takes in where the file will is specified to be copied from and to
642          * and ensures that there is no official sugar file there. If the file exists it will check
643          * against the MD5 file list to see if Sugar Created the file
644          *
645          */
646         function scanCopy($from, $to){
647                                 //if the file doesn't exist for the $to then it is not overriding anything
648                                 if(!file_exists($to))return;
649                                 //if $to is a dir and $from is a file then make $to a full file path as well
650                                 if(is_dir($to) && is_file($from)){
651                                         if(substr($to,-1) === '/'){
652                                                 $to = substr($to, 0 , strlen($to) - 1);
653                                         }
654                                         $to .= '/'. basename($from);
655                                 }
656                                 //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
657                                 if(is_file($to) && $this->sugarFileExists($to)){
658                                         $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
659                                 }
660
661                                 if(is_dir($from)){
662                                         $d = dir($from);
663                                         while($e = $d->read()){
664                                                 if($e == '.' || $e == '..')continue;
665                                                 $this->scanCopy($from .'/'. $e, $to .'/' . $e);
666                                         }
667                                 }
668
669
670
671
672
673                         }
674
675
676         /**
677          *Main external function that takes in a path to a package and then scans
678          *that package's manifest for disabled actions and then it scans the PHP files
679          *for restricted function calls
680          *
681          */
682         public function scanPackage($path){
683                 $this->pathToModule = $path;
684                 $this->scanManifest($path . '/manifest.php');
685                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableFileScan'])){
686                         $this->scanDir($path);
687                 }
688         }
689
690         /**
691          *This function will take all issues of the current instance and print them to the screen
692          **/
693         public function displayIssues($package='Package'){
694                 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').
695 '</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>.'.
696 '<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>';
697
698
699                 foreach($this->issues as $type=>$issues){
700                         echo '<div class="error"><h2>'. ucfirst($type) .' ' .  translate('ML_ISSUES') . '</h2> </div>';
701                         echo '<div id="details' . $type . '" >';
702                         foreach($issues as $file=>$issue){
703                                 $file = str_replace($this->pathToModule . '/', '', $file);
704                                 echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
705                                 if(is_array($issue)){
706                                         foreach($issue as $i){
707                                                 echo "$i<br>";
708                                         }
709                                 }else{
710                                         echo "$issue<br>";
711                                 }
712                                 echo "</div>";
713                         }
714                         echo '</div>';
715
716                 }
717                 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') . "\" />";
718
719         }
720
721
722 }
723
724
725 ?>