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