]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - ModuleInstall/ModuleScanner.php
Release 6.2.4
[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-2011 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         
61         private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
62         private $blackList = array(
63     'popen',
64     'proc_open',
65     'escapeshellarg',
66     'escapeshellcmd',
67     'proc_close',
68     'proc_get_status',
69     'proc_nice',
70     'basename',
71         'passthru',
72     'clearstatcache',
73     'delete',
74     'dirname',
75     'disk_free_space',
76     'disk_total_space',
77     'diskfreespace',
78     'fclose',
79     'feof',
80     'fflush',
81     'fgetc',
82     'fgetcsv',
83     'fgets',
84     'fgetss',
85     'file_exists',
86     'file_get_contents',
87     'filesize',
88     'filetype',
89     'flock',
90     'fnmatch',
91     'fpassthru',
92     'fputcsv',
93     'fputs',
94     'fread',
95     'fscanf',
96     'fseek',
97     'fstat',
98     'ftell',
99     'ftruncate',
100     'fwrite',
101     'glob',
102     'is_dir',
103     'is_file',
104     'is_link',
105     'is_readable',
106     'is_uploaded_file',
107     'parse_ini_string',
108     'pathinfo',
109     'pclose',
110     'readfile',
111     'readlink',
112     'realpath_cache_get',
113     'realpath_cache_size',
114     'realpath',
115     'rewind',
116     'set_file_buffer',
117     'tmpfile',
118     'umask',
119         'eval',
120         'exec',
121         'system',
122         'shell_exec',
123         'passthru', 
124         'chgrp',
125         'chmod', 
126         'chwown',
127         'file_put_contents',
128         'file',
129         'fileatime',
130         'filectime',
131         'filegroup',
132         'fileinode',
133         'filemtime',
134         'fileowner',
135         'fileperms',
136         'fopen', 
137         'is_executable',
138         'is_writable',
139         'is_writeable',
140         'lchgrp',
141         'lchown',
142         'linkinfo',
143         'lstat',
144         'mkdir',
145         'parse_ini_file',
146         'rmdir',
147         'stat',
148         'tempnam',
149         'touch',
150         'unlink',
151         'getimagesize',
152         'call_user_func',
153         'call_user_func_array',
154         'create_function',
155         
156
157         //mutliple files per function call
158         'copy',
159         'link',
160         'rename',
161         'symlink',
162         'move_uploaded_file',
163         'chdir',
164         'chroot',
165         'create_cache_directory',
166         'mk_temp_dir',
167         'write_array_to_file',
168         'write_encoded_file',
169         'create_custom_directory',
170         'sugar_rename',
171         'sugar_chown',
172         'sugar_fopen',
173         'sugar_mkdir',
174         'sugar_file_put_contents',
175         'sugar_chgrp',
176         'sugar_chmod',
177         'sugar_touch',
178
179 );
180
181         public function printToWiki(){
182                 echo "'''Default Extensions'''<br>";
183                 foreach($this->validExt as $b){
184                         echo '#' . $b . '<br>';
185                         
186                 }
187                 echo "'''Default Black Listed Functions'''<br>";
188                 foreach($this->blackList as $b){
189                         echo '#' . $b . '<br>';
190                         
191                 }
192                 
193         }
194
195         public function __construct(){
196                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackListExempt'])){
197                         $this->blackListExempt = array_merge($this->blackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['blackListExempt']);
198                 }
199                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackList'])){
200                         $this->blackList = array_merge($this->blackList, $GLOBALS['sugar_config']['moduleInstaller']['blackList']);
201                 }
202           if(!empty($GLOBALS['sugar_config']['moduleInstaller']['validExt'])){
203                         $this->validExt = array_merge($this->validExt, $GLOBALS['sugar_config']['moduleInstaller']['validExt']);
204                 }
205                 
206         }
207
208         private $issues = array();
209         private $pathToModule = '';
210         
211         /**
212          *returns a list of issues
213          */
214         public function getIssues(){
215                 return $this->issues;
216         }
217         
218         /**
219          *returns true or false if any issues were found
220          */
221         public function hasIssues(){
222                 return !empty($this->issues);
223         }
224         
225         /**
226          *Ensures that a file has a valid extension
227          */
228         private function isValidExtension($file){
229                 $file = strtolower($file);
230                 
231                 $extPos = strrpos($file, '.');
232                 //make sure they don't override the files.md5
233                 if($extPos === false || $file == 'files.md5')return false;
234                 $ext = substr($file, $extPos + 1);
235                 return in_array($ext, $this->validExt);
236                 
237         }
238         
239         /**
240          *Scans a directory and calls on scan file for each file  
241          **/
242         public function scanDir($path){
243                 static $startPath = '';
244                 if(empty($startPath))$startPath = $path;
245                 if(!is_dir($path))return false;
246                 $d = dir($path);
247                 while($e = $d->read()){
248                 $next = $path . '/' . $e;
249                 if(is_dir($next)){
250                         if(substr($e, 0, 1) == '.')continue;
251                         $this->scanDir($next);
252                 }else{ 
253                         $issues = $this->scanFile($next);
254                         
255                         
256                 }
257                 }
258         return true;
259         }
260         
261         
262         /**
263          * 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 
264          * $var()  and ` are always prevented then whatever is in the blacklist. 
265          * It will also ensure that all files are of valid extension types
266          * 
267          */
268         public function scanFile($file){
269                 $issues = array();
270                 if(!$this->isValidExtension($file)){
271                         $issues[] = translate('ML_INVALID_EXT');
272                         $this->issues['file'][$file] = $issues;
273                         return $issues;
274                 }
275                 $contents = file_get_contents($file);
276                 if(stripos($contents,'<?php') === false )return $issues;
277                 $tokens = token_get_all($contents);
278                 $checkFunction = false;
279                 $possibleIssue = '';
280                 $lastToken = false;
281                 foreach($tokens as $index=>$token){
282                         if(is_string($token[0])){
283                                 switch($token[0]){
284                                         case '`':
285                                                 $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
286                                         case '(':
287                                                 if($checkFunction)$issues[] = $possibleIssue;
288                                                 break;  
289                                 }
290                                 $checkFunction = false;
291                                 $possibleIssue = '';
292                         }else{
293                                 $token['_msi'] = token_name($token[0]);
294                                 switch($token[0]){
295                                         case T_WHITESPACE: continue;
296                                         case T_EVAL:
297                                                 if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
298                                                 $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
299                                                 break;
300                                         case T_STRING:
301                                                 $token[1] = strtolower($token[1]);
302                                                 if(!in_array($token[1], $this->blackList))break;
303                                                 if(in_array($token[1], $this->blackListExempt))break;
304                                                 if ($lastToken !== false && 
305                                                 ($lastToken[0] == T_NEW || $lastToken[0] == T_OBJECT_OPERATOR ||  $lastToken[0] == T_DOUBLE_COLON)) 
306                                                 {
307                                                         break;
308                                                 }
309                                         case T_VARIABLE:
310                                                 $checkFunction = true;
311                                                 $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' .  $token[1] . '()';
312                                                 break;
313                 
314                                         default:
315                                                 $checkFunction = false;
316                                                 $possibleIssue = '';
317
318                                 }
319                                 if ($token[0] != T_WHITESPACE)
320                                 {
321                                         $lastToken = $token;
322                                 }
323                         }
324                         
325                 }
326                 if(!empty($issues)){
327                         $this->issues['file'][$file] = $issues;
328                 }
329                 
330                 return $issues; 
331         }
332         
333         
334         /*
335          * checks files.md5 file to see if the file is from sugar 
336          * ONLY WORKS ON FILES
337          */
338         public function sugarFileExists($path){
339                 static $md5 = array();
340                 if(empty($md5) && file_exists('files.md5'))
341                 {
342                         include('files.md5');
343                         $md5 = $md5_string;
344                 }
345                 if(isset($md5['./' . $path]))return true;
346                 
347                 
348         }
349         
350         
351         /**
352          *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
353          *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
354          */
355         public function scanManifest($manifestPath){
356                 $issues = array();
357                 if(!file_exists($manifestPath)){
358                         $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
359                         return $issues;
360                 }
361                 $fileIssues = $this->scanFile($manifestPath);
362                 //if the manifest contains malicious code do not open it
363                 if(!empty($fileIssues)){
364                         return $fileIssues;
365                 }
366                 include($manifestPath);
367                 
368                         
369                 //scan for disabled actions 
370                 if(isset($GLOBALS['sugar_config']['moduleInstaller']['disableActions'])){
371                         foreach($GLOBALS['sugar_config']['moduleInstaller']['disableActions'] as $action){
372                                 if(isset($installdefs[$this->manifestMap[$action]])){
373                                         $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
374                                 }
375                         }
376                 }
377                 
378                 //now lets scan for files that will override our files 
379                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy']) && isset($installdefs['copy'])){
380                         foreach($installdefs['copy'] as $copy){
381                                 $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
382                                 $to = $copy['to'];
383                                 if(substr_count($from, '..')){
384                                         $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
385                                 }
386                                 if(substr_count($to, '..')){
387                                         $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
388                                 }
389                                 while(substr_count($from, '//')){
390                                         $from = str_replace('//', '/', $from);
391                                 }
392                                 while(substr_count($to, '//')){
393                                         $to = str_replace('//', '/', $to);
394                                 }       
395                                 $this->scanCopy($from, $to);
396                         }
397                 }
398                 if(!empty($issues)){
399                         $this->issues['manifest'][$manifestPath] = $issues;
400                 }
401                 
402                 
403                 
404         }
405         
406         
407
408         /**
409          * Takes in where the file will is specified to be copied from and to 
410          * and ensures that there is no official sugar file there. If the file exists it will check 
411          * against the MD5 file list to see if Sugar Created the file
412          * 
413          */
414         function scanCopy($from, $to){
415                                 //if the file doesn't exist for the $to then it is not overriding anything
416                                 if(!file_exists($to))return;
417                                 //if $to is a dir and $from is a file then make $to a full file path as well
418                                 if(is_dir($to) && is_file($from)){
419                                         if(substr($to,-1) === '/'){
420                                                 $to = substr($to, 0 , strlen($to) - 1);
421                                         }
422                                         $to .= '/'. basename($from);
423                                 }
424                                 //if the $to is a file and it is found in sugarFileExists then don't allow overriding it 
425                                 if(is_file($to) && $this->sugarFileExists($to)){
426                                         $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
427                                 }
428                                 
429                                 if(is_dir($from)){
430                                         $d = dir($from);
431                                         while($e = $d->read()){
432                                                 if($e == '.' || $e == '..')continue;
433                                                 $this->scanCopy($from .'/'. $e, $to .'/' . $e); 
434                                         }
435                                 }
436                                 
437                                 
438                                 
439                                 
440                                 
441                         }
442         
443         
444         /**
445          *Main external function that takes in a path to a package and then scans 
446          *that package's manifest for disabled actions and then it scans the PHP files
447          *for restricted function calls
448          *
449          */
450         public function scanPackage($path){
451                 $this->pathToModule = $path;
452                 $this->scanManifest($path . '/manifest.php');
453                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableFileScan'])){
454                         $this->scanDir($path);
455                 }
456         }
457         
458         /**
459          *This function will take all issues of the current instance and print them to the screen  
460          **/
461         public function displayIssues($package='Package'){
462                 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'). 
463 '</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>.'.
464 '<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>';
465
466                 
467                 foreach($this->issues as $type=>$issues){
468                         echo '<div class="error"><h2>'. ucfirst($type) .' ' .  translate('ML_ISSUES') . '</h2> </div>';
469                         echo '<div id="details' . $type . '" >'; 
470                         foreach($issues as $file=>$issue){
471                                 $file = str_replace($this->pathToModule . '/', '', $file);
472                                 echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">'; 
473                                 if(is_array($issue)){
474                                         foreach($issue as $i){
475                                                 echo "$i<br>";
476                                         }
477                                 }else{
478                                         echo "$issue<br>";
479                                 }
480                                 echo "</div>";
481                         }
482                         echo '</div>';
483                         
484                 }
485                 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') . "\" />";
486                 
487         }
488         
489         
490 }
491
492
493 ?>