XOOPS 2.5.6  Final
 All Classes Namespaces Files Functions Variables Pages
protector.php
Go to the documentation of this file.
1 <?php
2 
3 class Protector {
4 
5 var $mydirname ;
6 
7 var $_conn = null ;
8 var $_conf = array() ;
9 var $_conf_serialized = '' ;
10 
11 var $_bad_globals = array() ;
12 
13 var $message = '' ;
14 var $warning = false ;
15 var $error = false ;
16 var $_doubtful_requests = array() ;
17 var $_bigumbrella_doubtfuls = array() ;
18 
19 var $_dblayertrap_doubtfuls = array() ;
21  'information_schema' ,
22  'select' ,
23  "'" ,
24  '"' ,
25 ) ;
26 
27 var $_logged = false ;
28 
37 
42 
43 var $_spamcount_uri = 0 ;
44 
47 
48 var $_dos_stage = null ;
49 
50 var $ip_matched_info = null ;
51 
52 var $last_error_type = 'UNKNOWN' ;
53 
54 
55 // Constructor
56 function Protector()
57 {
58  $this->mydirname = 'protector' ;
59 
60  // Preferences from configs/cache
61  $this->_conf_serialized = @file_get_contents( $this->get_filepath4confighcache() ) ;
62  $this->_conf = @unserialize( $this->_conf_serialized ) ;
63  if( empty( $this->_conf ) ) $this->_conf = array() ;
64 
65  if( ! empty( $this->_conf['global_disabled'] ) ) return true ;
66 
67  // die if PHP_SELF XSS found (disabled in 2.53)
68 // if( preg_match( '/[<>\'";\n ]/' , @$_SERVER['PHP_SELF'] ) ) {
69 // $this->message .= "Invalid PHP_SELF '{$_SERVER['PHP_SELF']}' found.\n" ;
70 // $this->output_log( 'PHP_SELF XSS' ) ;
71 // die( 'invalid PHP_SELF' ) ;
72 // }
73 
74  // sanitize against PHP_SELF/PATH_INFO XSS (disabled in 3.33)
75 // $_SERVER['PHP_SELF'] = strtr( @$_SERVER['PHP_SELF'] , array( '<' => '%3C' , '>' => '%3E' , "'" => '%27' , '"' => '%22' ) ) ;
76 // if( ! empty( $_SERVER['PATH_INFO'] ) ) $_SERVER['PATH_INFO'] = strtr( @$_SERVER['PATH_INFO'] , array( '<' => '%3C' , '>' => '%3E' , "'" => '%27' , '"' => '%22' ) ) ;
77 
78  $this->_bad_globals = array( 'GLOBALS' , '_SESSION' , 'HTTP_SESSION_VARS' , '_GET' , 'HTTP_GET_VARS' , '_POST' , 'HTTP_POST_VARS' , '_COOKIE' , 'HTTP_COOKIE_VARS' , '_SERVER' , 'HTTP_SERVER_VARS' , '_REQUEST' , '_ENV' , '_FILES' , 'xoopsDB' , 'xoopsUser' , 'xoopsUserId' , 'xoopsUserGroups' , 'xoopsUserIsAdmin' , 'xoopsConfig' , 'xoopsOption' , 'xoopsModule' , 'xoopsModuleConfig' ) ;
79 
80  $this->_initial_recursive( $_GET , 'G' ) ;
81  $this->_initial_recursive( $_POST , 'P' ) ;
82  $this->_initial_recursive( $_COOKIE , 'C' ) ;
83 }
84 
85 
86 function _initial_recursive( $val , $key )
87 {
88  if( is_array( $val ) ) {
89  foreach( $val as $subkey => $subval ) {
90  // check bad globals
91  if( in_array( $subkey , $this->_bad_globals , true ) ) {
92  $this->message .= "Attempt to inject '$subkey' was found.\n" ;
93  $this->_safe_contami = false ;
94  $this->last_error_type = 'CONTAMI' ;
95  }
96  $this->_initial_recursive( $subval , $key . '_' . base64_encode( $subkey ) ) ;
97  }
98  } else {
99  // check nullbyte attack
100  if( @$this->_conf['san_nullbyte'] && strstr( $val , chr(0) ) ) {
101  $val = str_replace( chr(0) , ' ' , $val ) ;
102  $this->replace_doubtful( $key , $val ) ;
103  $this->message .= "Injecting Null-byte '$val' found.\n" ;
104  $this->output_log( 'NullByte' , 0 , false , 32 ) ;
105  // $this->purge() ;
106  }
107 
108  // register as doubtful requests against SQL Injections
109  if( preg_match( '?[\s\'"`/]?' , $val ) ) {
110  $this->_doubtful_requests["$key"] = $val ;
111  }
112  }
113 }
114 
115 
116 static function &getInstance()
117 {
118  static $instance ;
119  if( ! isset( $instance ) ) {
120  $instance = new Protector() ;
121  }
122  return $instance ;
123 }
124 
125 
127 {
128  $constpref = '_MI_' . strtoupper( $this->mydirname ) ;
129 
130  if( empty( $this->_conn ) ) return false ;
131 
132  $result = @mysql_query( "SELECT conf_name,conf_value FROM ".XOOPS_DB_PREFIX."_config WHERE conf_title like '".$constpref."%'" , $this->_conn ) ;
133  if( ! $result || mysql_num_rows( $result ) < 5 ) {
134  return false ;
135  }
136  $db_conf = array() ;
137  while( list( $key , $val ) = mysql_fetch_row( $result ) ) {
138  $db_conf[ $key ] = $val ;
139  }
140  $db_conf_serialized = serialize( $db_conf ) ;
141 
142  // update config cache
143  if( $db_conf_serialized != $this->_conf_serialized ) {
144  $fp = fopen( $this->get_filepath4confighcache() , 'w' ) ;
145  fwrite( $fp , $db_conf_serialized ) ;
146  fclose( $fp ) ;
147  $this->_conf = $db_conf ;
148  }
149  return true ;
150 }
151 
152 
153 function setConn( $conn )
154 {
155  $this->_conn = $conn ;
156 }
157 
158 
159 function getConf()
160 {
161  return $this->_conf ;
162 }
163 
164 
165 function purge( $redirect_to_top = false )
166 {
167  // clear all session values
168  if( isset( $_SESSION ) ) foreach( $_SESSION as $key => $val ) {
169  $_SESSION[ $key ] = '' ;
170  if( isset( $GLOBALS[ $key ] ) ) $GLOBALS[ $key ] = '' ;
171  }
172 
173  if( ! headers_sent() ) {
174  // clear typical session id of PHP
175  setcookie( 'PHPSESSID' , '' , time() - 3600 , '/' , '' , 0 ) ;
176  if( isset( $_COOKIE[ session_name() ] ) ) {
177  setcookie( session_name() , '' , time() - 3600 , '/' , '' , 0 ) ;
178  }
179 
180  // clear autologin cookie
181  $xoops_cookie_path = defined('XOOPS_COOKIE_PATH') ? XOOPS_COOKIE_PATH : preg_replace( '?http://[^/]+(/.*)$?' , "$1" , XOOPS_URL ) ;
182  if( $xoops_cookie_path == XOOPS_URL ) $xoops_cookie_path = '/' ;
183  setcookie('autologin_uname', '', time() - 3600, $xoops_cookie_path, '', 0);
184  setcookie('autologin_pass', '', time() - 3600, $xoops_cookie_path, '', 0);
185  }
186 
187  if( $redirect_to_top ) {
188  header( 'Location: '.XOOPS_URL.'/' ) ;
189  exit ;
190  } else {
191  $ret = $this->call_filter( 'prepurge_exit' ) ;
192  if( $ret == false ) {
193  die( 'Protector detects attacking actions' ) ;
194  }
195  }
196 }
197 
198 
199 function output_log( $type = 'UNKNOWN' , $uid = 0 , $unique_check = false , $level = 1 )
200 {
201  if( $this->_logged ) return true ;
202 
203  if( ! ( $this->_conf['log_level'] & $level ) ) return true ;
204 
205  if( empty( $this->_conn ) ) {
206  $this->_conn = @mysql_connect( XOOPS_DB_HOST , XOOPS_DB_USER , XOOPS_DB_PASS ) ;
207  if( ! $this->_conn ) die( 'db connection failed.' ) ;
208  if( ! mysql_select_db( XOOPS_DB_NAME , $this->_conn ) ) die( 'db selection failed.' ) ;
209  }
210 
211  $ip = @$_SERVER['REMOTE_ADDR'] ;
212  $agent = @$_SERVER['HTTP_USER_AGENT'] ;
213 
214  if( $unique_check ) {
215  $result = mysql_query( 'SELECT ip,type FROM '.XOOPS_DB_PREFIX.'_'.$this->mydirname.'_log ORDER BY timestamp DESC LIMIT 1' , $this->_conn ) ;
216  list( $last_ip , $last_type ) = mysql_fetch_row( $result ) ;
217  if( $last_ip == $ip && $last_type == $type ) {
218  $this->_logged = true ;
219  return true ;
220  }
221  }
222 
223  mysql_query( "INSERT INTO ".XOOPS_DB_PREFIX."_".$this->mydirname."_log SET ip='".addslashes($ip)."',agent='".addslashes($agent)."',type='".addslashes($type)."',description='".addslashes($this->message)."',uid='".intval($uid)."',timestamp=NOW()" , $this->_conn ) ;
224  $this->_logged = true ;
225  return true ;
226 }
227 
228 
229 function write_file_bwlimit( $expire )
230 {
231  $expire = min( intval( $expire ) , time() + 300 ) ;
232 
233  $fp = @fopen( $this->get_filepath4bwlimit() , 'w' ) ;
234  if( $fp ) {
235  @flock( $fp , LOCK_EX ) ;
236  fwrite( $fp , $expire . "\n" ) ;
237  @flock( $fp , LOCK_UN ) ;
238  fclose( $fp ) ;
239  return true ;
240  } else {
241  return false ;
242  }
243 }
244 
245 
246 function get_bwlimit()
247 {
248  list( $expire ) = @file( Protector::get_filepath4bwlimit() ) ;
249  $expire = min( intval( $expire ) , time() + 300 ) ;
250 
251  return $expire ;
252 }
253 
254 
255 static function get_filepath4bwlimit()
256 {
257  return XOOPS_TRUST_PATH . '/modules/protector/configs/bwlimit' . substr( md5( XOOPS_ROOT_PATH . XOOPS_DB_USER . XOOPS_DB_PREFIX ) , 0 , 6 ) ;
258 }
259 
260 
262 {
263  asort( $bad_ips ) ;
264 
265  $fp = @fopen( $this->get_filepath4badips() , 'w' ) ;
266  if( $fp ) {
267  @flock( $fp , LOCK_EX ) ;
268  fwrite( $fp , serialize( $bad_ips ) . "\n" ) ;
269  @flock( $fp , LOCK_UN ) ;
270  fclose( $fp ) ;
271  return true ;
272  } else {
273  return false ;
274  }
275 }
276 
277 
278 function register_bad_ips( $jailed_time = 0 , $ip = null )
279 {
280  if( empty( $ip ) ) $ip = @$_SERVER['REMOTE_ADDR'] ;
281  if( empty( $ip ) ) return false ;
282 
283  $bad_ips = $this->get_bad_ips( true ) ;
284  $bad_ips[ $ip ] = $jailed_time ? $jailed_time : 0x7fffffff ;
285 
286  return $this->write_file_badips( $bad_ips ) ;
287 }
288 
289 
290 function get_bad_ips( $with_jailed_time = false )
291 {
292  list( $bad_ips_serialized ) = @file( Protector::get_filepath4badips() ) ;
293  $bad_ips = empty( $bad_ips_serialized ) ? array() : @unserialize( $bad_ips_serialized ) ;
294  if( ! is_array( $bad_ips ) || isset( $bad_ips[0] ) ) $bad_ips = array() ;
295 
296  // expire jailed_time
297  $pos = 0 ;
298  foreach( $bad_ips as $bad_ip => $jailed_time ) {
299  if( $jailed_time >= time() ) break ;
300  $pos ++ ;
301  }
302  $bad_ips = array_slice( $bad_ips , $pos ) ;
303 
304  if( $with_jailed_time ) {
305  return $bad_ips ;
306  } else {
307  return array_keys( $bad_ips ) ;
308  }
309 }
310 
311 
312 static function get_filepath4badips()
313 {
314  return XOOPS_TRUST_PATH . '/modules/protector/configs/badips' . substr( md5( XOOPS_ROOT_PATH . XOOPS_DB_USER . XOOPS_DB_PREFIX ) , 0 , 6 ) ;
315 }
316 
317 
318 function get_group1_ips( $with_info = false )
319 {
320  list( $group1_ips_serialized ) = @file( Protector::get_filepath4group1ips() ) ;
321  $group1_ips = empty( $group1_ips_serialized ) ? array() : @unserialize( $group1_ips_serialized ) ;
322  if( ! is_array( $group1_ips ) ) $group1_ips = array() ;
323 
324  if( $with_info ) {
325  $group1_ips = array_flip( $group1_ips ) ;
326  }
327 
328  return $group1_ips ;
329 }
330 
331 
332 static function get_filepath4group1ips()
333 {
334  return XOOPS_TRUST_PATH . '/modules/protector/configs/group1ips' . substr( md5( XOOPS_ROOT_PATH . XOOPS_DB_USER . XOOPS_DB_PREFIX ) , 0 , 6 ) ;
335 }
336 
337 
339 {
340  return XOOPS_TRUST_PATH . '/modules/protector/configs/configcache' . substr( md5( XOOPS_ROOT_PATH . XOOPS_DB_USER . XOOPS_DB_PREFIX ) , 0 , 6 ) ;
341 }
342 
343 
344 function ip_match( $ips )
345 {
346  foreach( $ips as $ip => $info ) {
347  if( $ip ) {
348  switch( substr( $ip , -1 ) ) {
349  case '.' :
350  // foward match
351  if( substr( @$_SERVER['REMOTE_ADDR'] , 0 , strlen( $ip ) ) == $ip ) {
352  $this->ip_matched_info = $info ;
353  return true ;
354  }
355  break ;
356  case '0' :
357  case '1' :
358  case '2' :
359  case '3' :
360  case '4' :
361  case '5' :
362  case '6' :
363  case '7' :
364  case '8' :
365  case '9' :
366  // full match
367  if( @$_SERVER['REMOTE_ADDR'] == $ip ) {
368  $this->ip_matched_info = $info ;
369  return true ;
370  }
371  break ;
372  default :
373  // perl regex
374  if( @preg_match( $ip , @$_SERVER['REMOTE_ADDR'] ) ) {
375  $this->ip_matched_info = $info ;
376  return true ;
377  }
378  break ;
379  }
380  }
381  }
382  $this->ip_matched_info = null ;
383  return false ;
384 }
385 
386 
387 function deny_by_htaccess( $ip = null )
388 {
389  if( empty( $ip ) ) $ip = @$_SERVER['REMOTE_ADDR'] ;
390  if( empty( $ip ) ) return false ;
391  if( ! function_exists( 'file_get_contents' ) ) return false ;
392 
393  $target_htaccess = XOOPS_ROOT_PATH.'/.htaccess' ;
394  $backup_htaccess = XOOPS_ROOT_PATH.'/uploads/.htaccess.bak' ;
395 
396  $ht_body = file_get_contents( $target_htaccess ) ;
397 
398  // make backup as uploads/.htaccess.bak automatically
399  if( $ht_body && ! file_exists( $backup_htaccess ) ) {
400  $fw = fopen( $backup_htaccess , "w" ) ;
401  fwrite( $fw , $ht_body ) ;
402  fclose( $fw ) ;
403  }
404 
405  // if .htaccess is broken, restore from backup
406  if( ! $ht_body && file_exists( $backup_htaccess ) ) {
407  $ht_body = file_get_contents( $backup_htaccess ) ;
408  }
409 
410  // new .htaccess
411  if( $ht_body === false ) {
412  $ht_body = '' ;
413  }
414 
415  if( preg_match( "/^(.*)#PROTECTOR#\s+(DENY FROM .*)\n#PROTECTOR#\n(.*)$/si" , $ht_body , $regs ) ) {
416  if( substr( $regs[2] , - strlen( $ip ) ) == $ip ) return true ;
417  $new_ht_body = $regs[1] . "#PROTECTOR#\n" . $regs[2] . " $ip\n#PROTECTOR#\n" . $regs[3] ;
418  } else {
419  $new_ht_body = "#PROTECTOR#\nDENY FROM $ip\n#PROTECTOR#\n" . $ht_body ;
420  }
421 
422  // error_log( "$new_ht_body\n" , 3 , "/tmp/error_log" ) ;
423 
424  $fw = fopen( $target_htaccess , "w" ) ;
425  @flock( $fw , LOCK_EX ) ;
426  fwrite( $fw , $new_ht_body ) ;
427  @flock( $fw , LOCK_UN ) ;
428  fclose( $fw ) ;
429 
430  return true ;
431 }
432 
433 
435 {
437 }
438 
439 
441 {
442  if( is_array( $val ) ) {
443  foreach( $val as $subval ) {
444  $this->_dblayertrap_check_recursive( $subval ) ;
445  }
446  } else {
447  if( strlen( $val ) < 6 ) return ;
448  $val = get_magic_quotes_gpc() ? stripslashes( $val ) : $val ;
449  foreach( $this->_dblayertrap_doubtful_needles as $needle ) {
450  if( stristr( $val , $needle ) ) {
451  $this->_dblayertrap_doubtfuls[] = $val ;
452  }
453  }
454  }
455 }
456 
457 
458 function dblayertrap_init( $force_override = false )
459 {
460  if( ! empty( $GLOBALS['xoopsOption']['nocommon'] ) || defined( '_LEGACY_PREVENT_EXEC_COMMON_' ) || defined( '_LEGACY_PREVENT_LOAD_CORE_' ) ) return ; // skip
461 
462  $this->_dblayertrap_doubtfuls = array() ;
465  $this->_dblayertrap_check_recursive( $_COOKIE ) ;
466  if( empty( $this->_conf['dblayertrap_wo_server'] ) ) {
468  }
469 
470  if( ! empty( $this->_dblayertrap_doubtfuls ) || $force_override ) {
471  @define('XOOPS_DB_ALTERNATIVE' , 'ProtectorMysqlDatabase' ) ;
472  require_once dirname(dirname(__FILE__)).'/class/ProtectorMysqlDatabase.class.php' ;
473  }
474 }
475 
476 
478 {
479  if( is_array( $val ) ) {
480  foreach( $val as $subval ) {
481  $this->_bigumbrella_check_recursive( $subval ) ;
482  }
483  } else {
484  if( preg_match( '/[<\'"].{15}/s' , $val , $regs ) ) {
485  $this->_bigumbrella_doubtfuls[] = $regs[0] ;
486  }
487  }
488 }
489 
490 
492 {
493  $this->_bigumbrella_doubtfuls = array() ;
495  $this->_bigumbrella_check_recursive( @$_SERVER['PHP_SELF'] ) ;
496 
497  if( ! empty( $this->_bigumbrella_doubtfuls ) ) {
498  ob_start( array( $this , 'bigumbrella_outputcheck' ) ) ;
499  }
500 }
501 
502 
504 {
505  if( defined( 'BIGUMBRELLA_DISABLED' ) ) return $s ;
506 
507  if( function_exists( 'headers_list' ) ) {
508  foreach( headers_list() as $header ) {
509  if( stristr( $header , 'Content-Type:' ) && ! stristr( $header , 'text/html' ) ) {
510  return $s ;
511  }
512  }
513  }
514 
515  if( ! is_array( $this->_bigumbrella_doubtfuls ) ) {
516  return "bigumbrella injection found." ;
517  }
518 
519  foreach( $this->_bigumbrella_doubtfuls as $doubtful ) {
520  if( strstr( $s , $doubtful ) ) {
521  return "XSS found by Protector." ;
522  }
523  }
524  return $s ;
525 }
526 
527 
529 {
530  global $HTTP_GET_VARS , $HTTP_POST_VARS , $HTTP_COOKIE_VARS ;
531 
532  if( $this->_done_intval ) return true ;
533  else $this->_done_intval = true ;
534 
535  foreach( $_GET as $key => $val ) {
536  if( substr( $key , -2 ) == 'id' && ! is_array( $_GET[ $key ] ) ) {
537  $newval = preg_replace( '/[^0-9a-zA-Z_-]/' , '' , $val ) ;
538  $_GET[ $key ] = $HTTP_GET_VARS[ $key ] = $newval ;
539  if( $_REQUEST[ $key ] == $_GET[ $key ] ){
540  $_REQUEST[ $key ] = $newval ;
541  }
542  }
543  }
544  foreach( $_POST as $key => $val ) {
545  if( substr( $key , -2 ) == 'id' && ! is_array( $_POST[ $key ] ) ) {
546  $newval = preg_replace( '/[^0-9a-zA-Z_-]/' , '' , $val ) ;
547  $_POST[ $key ] = $HTTP_POST_VARS[ $key ] = $newval ;
548  if( $_REQUEST[ $key ] == $_POST[ $key ] ){
549  $_REQUEST[ $key ] = $newval ;
550  }
551  }
552  }
553  foreach( $_COOKIE as $key => $val ) {
554  if( substr( $key , -2 ) == 'id' && ! is_array( $_COOKIE[ $key ] ) ) {
555  $newval = preg_replace( '/[^0-9a-zA-Z_-]/' , '' , $val ) ;
556  $_COOKIE[ $key ] = $HTTP_COOKIE_VARS[ $key ] = $newval ;
557  if( $_REQUEST[ $key ] == $_COOKIE[ $key ] ){
558  $_REQUEST[ $key ] = $newval ;
559  }
560  }
561  }
562 
563  return true ;
564 }
565 
566 
568 {
569  global $HTTP_GET_VARS , $HTTP_POST_VARS , $HTTP_COOKIE_VARS ;
570 
571  if( $this->_done_dotdot ) return true ;
572  else $this->_done_dotdot = true ;
573 
574  foreach( $_GET as $key => $val ) {
575  if( is_array( $_GET[ $key ] ) ) continue ;
576  if( substr( trim( $val ) , 0 , 3 ) == '../' || strstr( $val , '../../' ) ) {
577  $this->last_error_type = 'DirTraversal' ;
578  $this->message .= "Directory Traversal '$val' found.\n" ;
579  $this->output_log( $this->last_error_type , 0 , false , 64 ) ;
580  $sanitized_val = str_replace( chr(0) , '' , $val ) ;
581  if( substr( $sanitized_val , -2 ) != ' .' ) $sanitized_val .= ' .' ;
582  $_GET[ $key ] = $HTTP_GET_VARS[ $key ] = $sanitized_val ;
583  if( $_REQUEST[ $key ] == $_GET[ $key ] ){
584  $_REQUEST[ $key ] = $sanitized_val ;
585  }
586  }
587  }
588 /* foreach( $_POST as $key => $val ) {
589  if( is_array( $_POST[ $key ] ) ) continue ;
590  if( substr( trim( $val ) , 0 , 3 ) == '../' || strstr( $val , '../../' ) ) {
591  $this->last_error_type = 'ParentDir' ;
592  $this->message .= "Doubtful file specification '$val' found.\n" ;
593  $this->output_log( $this->last_error_type , 0 , false , 128 ) ;
594  $sanitized_val = str_replace( chr(0) , '' , $val ) ;
595  if( substr( $sanitized_val , -2 ) != ' .' ) $sanitized_val .= ' .' ;
596  $_POST[ $key ] = $HTTP_POST_VARS[ $key ] = $sanitized_val ;
597  if( $_REQUEST[ $key ] == $_POST[ $key ] ){
598  $_REQUEST[ $key ] = $sanitized_val ;
599  }
600  }
601  }
602  foreach( $_COOKIE as $key => $val ) {
603  if( is_array( $_COOKIE[ $key ] ) ) continue ;
604  if( substr( trim( $val ) , 0 , 3 ) == '../' || strstr( $val , '../../' ) ) {
605  $this->last_error_type = 'ParentDir' ;
606  $this->message .= "Doubtful file specification '$val' found.\n" ;
607  $this->output_log( $this->last_error_type , 0 , false , 128 ) ;
608  $sanitized_val = str_replace( chr(0) , '' , $val ) ;
609  if( substr( $sanitized_val , -2 ) != ' .' ) $sanitized_val .= ' .' ;
610  $_COOKIE[ $key ] = $HTTP_COOKIE_VARS[ $key ] = $sanitized_val ;
611  if( $_REQUEST[ $key ] == $_COOKIE[ $key ] ){
612  $_REQUEST[ $key ] = $sanitized_val ;
613  }
614  }
615  }*/
616 
617  return true ;
618 }
619 
620 
621 function &get_ref_from_base64index( &$current , $indexes )
622 {
623  foreach( $indexes as $index ) {
624  $index = base64_decode( $index ) ;
625  if( ! is_array( $current ) ) return false ;
626  $current =& $current[ $index ] ;
627  }
628  return $current ;
629 }
630 
631 
632 function replace_doubtful( $key , $val )
633 {
634  global $HTTP_GET_VARS , $HTTP_POST_VARS , $HTTP_COOKIE_VARS ;
635 
636  $index_expression = '' ;
637  $indexes = explode( '_' , $key ) ;
638  $base_array = array_shift( $indexes ) ;
639 
640  switch( $base_array ) {
641  case 'G' :
642  $main_ref =& $this->get_ref_from_base64index( $_GET , $indexes ) ;
643  $legacy_ref =& $this->get_ref_from_base64index( $HTTP_GET_VARS , $indexes ) ;
644  break ;
645  case 'P' :
646  $main_ref =& $this->get_ref_from_base64index( $_POST , $indexes ) ;
647  $legacy_ref =& $this->get_ref_from_base64index( $HTTP_POST_VARS , $indexes ) ;
648  break ;
649  case 'C' :
650  $main_ref =& $this->get_ref_from_base64index( $_COOKIE , $indexes ) ;
651  $legacy_ref =& $this->get_ref_from_base64index( $HTTP_COOKIE_VARS , $indexes ) ;
652  break ;
653  default :
654  exit ;
655  }
656  if( ! isset( $main_ref ) ) exit ;
657  $request_ref =& $this->get_ref_from_base64index( $_REQUEST , $indexes ) ;
658  if( $request_ref !== false && $main_ref == $request_ref ) {
659  $request_ref = $val ;
660  }
661  $main_ref = $val ;
662  $legacy_ref = $val ;
663 }
664 
665 
667 {
668  if( $this->_done_badext ) return $this->_safe_badext ;
669  else $this->_done_badext = true ;
670 
671  // extensions never uploaded
672  $bad_extensions = array( 'php' , 'phtml' , 'phtm' , 'php3' , 'php4' , 'cgi' , 'pl' , 'asp' ) ;
673  // extensions needed image check (anti-IE Content-Type XSS)
674  $image_extensions = array( 1 => 'gif', 2 => 'jpg', 3 => 'png', 4 => 'swf', 5 => 'psd', 6 => 'bmp', 7 => 'tif', 8 => 'tif', 9 => 'jpc', 10 => 'jp2', 11 => 'jpx', 12 => 'jb2', 13 => 'swc', 14 => 'iff', 15 => 'wbmp', 16 => 'xbm' ) ;
675 
676  foreach( $_FILES as $_file ) {
677  if( ! empty( $_file['error'] ) ) continue ;
678  if( ! empty( $_file['name'] ) && is_string( $_file['name'] ) ) {
679  $ext = strtolower( substr( strrchr( $_file['name'] , '.' ) , 1 ) ) ;
680  if( $ext == 'jpeg' ) $ext = 'jpg' ;
681  else if( $ext == 'tiff' ) $ext = 'tif' ;
682 
683  // anti multiple dot file (Apache mod_mime.c)
684  if( count( explode( '.' , str_replace( '.tar.gz' , '.tgz' , $_file['name'] ) ) ) > 2 ) {
685  $this->message .= "Attempt to multiple dot file {$_file['name']}.\n" ;
686  $this->_safe_badext = false ;
687  $this->last_error_type = 'UPLOAD' ;
688  }
689 
690  // anti dangerous extensions
691  if( in_array( $ext , $bad_extensions ) ) {
692  $this->message .= "Attempt to upload {$_file['name']}.\n" ;
693  $this->_safe_badext = false ;
694  $this->last_error_type = 'UPLOAD' ;
695  }
696 
697  // anti camouflaged image file
698  if( in_array( $ext , $image_extensions ) ) {
699  $image_attributes = @getimagesize( $_file['tmp_name'] ) ;
700  if( $image_attributes === false && is_uploaded_file( $_file['tmp_name'] ) ) {
701  // open_basedir restriction
702  $temp_file = XOOPS_ROOT_PATH.'/uploads/protector_upload_temporary'.md5( time() ) ;
703  move_uploaded_file( $_file['tmp_name'] , $temp_file ) ;
704  $image_attributes = @getimagesize( $temp_file ) ;
705  @unlink( $temp_file ) ;
706  }
707 
708  if( $image_attributes === false || $image_extensions[ intval( $image_attributes[2] ) ] != $ext ) {
709  $this->message .= "Attempt to upload camouflaged image file {$_file['name']}.\n" ;
710  $this->_safe_badext = false ;
711  $this->last_error_type = 'UPLOAD' ;
712  }
713  }
714  }
715  }
716 
717  return $this->_safe_badext ;
718 }
719 
720 
722 {
723 /* if( $this->_done_contami ) return $this->_safe_contami ;
724  else $this->_done_contami = true ; */
725 
726 /* foreach( $this->_bad_globals as $bad_global ) {
727  if( isset( $_REQUEST[ $bad_global ] ) ) {
728  $this->message .= "Attempt to inject '$bad_global' was found.\n" ;
729  $this->_safe_contami = false ;
730  $this->last_error_type = 'CONTAMI' ;
731  }
732  }*/
733 
734  return $this->_safe_contami ;
735 }
736 
737 
738 function check_sql_isolatedcommentin( $sanitize = true )
739 {
740  if( $this->_done_isocom ) return $this->_safe_isocom ;
741  else $this->_done_isocom = true ;
742 
743  foreach( $this->_doubtful_requests as $key => $val ) {
744  $str = $val ;
745  while( $str = strstr( $str , '/*' ) ) { /* */
746  $str = strstr( substr( $str , 2 ) , '*/' ) ;
747  if( $str === false ) {
748  $this->message .= "Isolated comment-in found. ($val)\n" ;
749  if( $sanitize ) $this->replace_doubtful( $key , $val . '*/' ) ;
750  $this->_safe_isocom = false ;
751  $this->last_error_type = 'ISOCOM' ;
752  }
753  }
754  }
755  return $this->_safe_isocom ;
756 }
757 
758 
759 function check_sql_union( $sanitize = true )
760 {
761  if( $this->_done_union ) return $this->_safe_union ;
762  else $this->_done_union = true ;
763 
764  foreach( $this->_doubtful_requests as $key => $val ) {
765 
766  $str = str_replace( array( '/*' , '*/' ) , '' , preg_replace( '?/\*.+\*/?sU' , '' , $val ) ) ;
767  if( preg_match( '/\sUNION\s+(ALL|SELECT)/i' , $str ) ) {
768  $this->message .= "Pattern like SQL injection found. ($val)\n" ;
769  if( $sanitize ) $this->replace_doubtful( $key , preg_replace( '/union/i' , 'uni-on' , $val ) ) ;
770  $this->_safe_union = false ;
771  $this->last_error_type = 'UNION' ;
772  }
773  }
774  return $this->_safe_union ;
775 }
776 
778 {
779  if (!function_exists('curl_init')) return false;
780 
781  if ($_SERVER['REQUEST_METHOD'] != 'POST') return false;
782 
783  $query = "f=serial&ip=" . $_SERVER['REMOTE_ADDR'];
784  $query .= isset($_POST['email']) ? "&email=" . $_POST['email'] : '';
785  $query .= isset($_POST['uname']) ? "&username=" . $_POST['uname'] : '';
786  $url = "http://www.stopforumspam.com/api?" . $query;
787  $ch = curl_init();
788  curl_setopt($ch, CURLOPT_URL, $url);
789  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
790  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
791  $result = unserialize(curl_exec($ch));
792  curl_close($ch);
793 
794  $spammer = false;
795  if (isset($result['email']) && isset($result['email']['lastseen'])) {
796  $spammer = true;
797  }
798 
799  if (isset($result['ip']) && isset($result['ip']['lastseen'])) {
800  $last = strtotime($result['ip']['lastseen']);
801  $oneMonth = 60*60*24*31;
802  $oneMonthAgo = time() - $oneMonth;
803  if ($last > $oneMonthAgo) {
804  $spammer = true;
805  }
806  }
807 
808  if (!$spammer) return false;
809 
810  $this->last_error_type = 'SPAMMER POST' ;
811 
812  switch ($this->_conf['stopforumspam_action']) {
813  default :
814  case 'log' :
815  break;
816  case 'san' :
817  $_POST = array();
818  $this->message .= "POST deleted for IP:" . $_SERVER['REMOTE_ADDR'];
819  break;
820  case 'biptime0' :
821  $_POST = array();
822  $this->message .= "BAN and POST deleted for IP:" . $_SERVER['REMOTE_ADDR'];
823  $this->_should_be_banned_time0 = true;
824  break;
825  case 'bip' :
826  $_POST = array();
827  $this->message .= "Ban and POST deleted for IP:" . $_SERVER['REMOTE_ADDR'];
828  $this->_should_be_banned = true;
829  break;
830  }
831 
832  $this->output_log($this->last_error_type, $uid, false, 16);
833 
834  return true;
835 }
836 
837 
838 function check_dos_attack( $uid = 0 , $can_ban = false )
839 {
840  global $xoopsDB ;
841 
842  if( $this->_done_dos ) return true ;
843 
844  $ip = @$_SERVER['REMOTE_ADDR'] ;
845  $uri = @$_SERVER['REQUEST_URI'] ;
846  $ip4sql = addslashes( $ip ) ;
847  $uri4sql = addslashes( $uri ) ;
848  if( empty( $ip ) || $ip == '' ) return true ;
849 
850  // gargage collection
851  $result = $xoopsDB->queryF( "DELETE FROM ".$xoopsDB->prefix($this->mydirname."_access")." WHERE expire < UNIX_TIMESTAMP()" ) ;
852 
853  // for older versions before updating this module
854  if( $result === false ) {
855  $this->_done_dos = true ;
856  return true ;
857  }
858 
859  // sql for recording access log (INSERT should be placed after SELECT)
860  $sql4insertlog = "INSERT INTO ".$xoopsDB->prefix($this->mydirname."_access")." SET ip='$ip4sql',request_uri='$uri4sql',expire=UNIX_TIMESTAMP()+'".intval($this->_conf['dos_expire'])."'" ;
861 
862  // bandwidth limitation
863  if( @$this->_conf['bwlimit_count'] >= 10 ) {
864  $result = $xoopsDB->query( "SELECT COUNT(*) FROM ".$xoopsDB->prefix($this->mydirname."_access") ) ;
865  list( $bw_count ) = $xoopsDB->fetchRow( $result ) ;
866  if( $bw_count > $this->_conf['bwlimit_count'] ) {
867  $this->write_file_bwlimit( time() + $this->_conf['dos_expire'] ) ;
868  }
869  }
870 
871  // F5 attack check (High load & same URI)
872  $result = $xoopsDB->query( "SELECT COUNT(*) FROM ".$xoopsDB->prefix($this->mydirname."_access")." WHERE ip='$ip4sql' AND request_uri='$uri4sql'" ) ;
873  list( $f5_count ) = $xoopsDB->fetchRow( $result ) ;
874  if( $f5_count > $this->_conf['dos_f5count'] ) {
875 
876  // delayed insert
877  $xoopsDB->queryF( $sql4insertlog ) ;
878 
879  // extends the expires of the IP with 5 minutes at least (pending)
880  // $result = $xoopsDB->queryF( "UPDATE ".$xoopsDB->prefix($this->mydirname."_access")." SET expire=UNIX_TIMESTAMP()+300 WHERE ip='$ip4sql' AND expire<UNIX_TIMESTAMP()+300" ) ;
881 
882  // call the filter first
883  $ret = $this->call_filter( 'f5attack_overrun' ) ;
884 
885  // actions for F5 Attack
886  $this->_done_dos = true ;
887  $this->last_error_type = 'DoS' ;
888  switch( $this->_conf['dos_f5action'] ) {
889  default :
890  case 'exit' :
891  $this->output_log( $this->last_error_type , $uid , true , 16 ) ;
892  exit ;
893  case 'none' :
894  $this->output_log( $this->last_error_type , $uid , true , 16 ) ;
895  return true ;
896  case 'biptime0' :
897  if( $can_ban ) $this->register_bad_ips( time() + $this->_conf['banip_time0'] ) ;
898  break ;
899  case 'bip' :
900  if( $can_ban ) $this->register_bad_ips() ;
901  break ;
902  case 'hta' :
903  if( $can_ban ) $this->deny_by_htaccess() ;
904  break ;
905  case 'sleep' :
906  sleep( 5 ) ;
907  break ;
908  }
909  return false ;
910  }
911 
912  // Check its Agent
913  if( trim( $this->_conf['dos_crsafe'] ) != '' && preg_match( $this->_conf['dos_crsafe'] , @$_SERVER['HTTP_USER_AGENT'] ) ) {
914  // welcomed crawler
915  $this->_done_dos = true ;
916  return true ;
917  }
918 
919  // Crawler check (High load & different URI)
920  $result = $xoopsDB->query( "SELECT COUNT(*) FROM ".$xoopsDB->prefix($this->mydirname."_access")." WHERE ip='$ip4sql'" ) ;
921  list( $crawler_count ) = $xoopsDB->fetchRow( $result ) ;
922 
923  // delayed insert
924  $xoopsDB->queryF( $sql4insertlog ) ;
925 
926  if( $crawler_count > $this->_conf['dos_crcount'] ) {
927 
928  // call the filter first
929  $ret = $this->call_filter( 'crawler_overrun' ) ;
930 
931  // actions for bad Crawler
932  $this->_done_dos = true ;
933  $this->last_error_type = 'CRAWLER' ;
934  switch( $this->_conf['dos_craction'] ) {
935  default :
936  case 'exit' :
937  $this->output_log( $this->last_error_type , $uid , true , 16 ) ;
938  exit ;
939  case 'none' :
940  $this->output_log( $this->last_error_type , $uid , true , 16 ) ;
941  return true ;
942  case 'biptime0' :
943  if( $can_ban ) $this->register_bad_ips( time() + $this->_conf['banip_time0'] ) ;
944  break ;
945  case 'bip' :
946  if( $can_ban ) $this->register_bad_ips() ;
947  break ;
948  case 'hta' :
949  if( $can_ban ) $this->deny_by_htaccess() ;
950  break ;
951  case 'sleep' :
952  sleep( 5 ) ;
953  break ;
954  }
955  return false ;
956  }
957 
958  return true ;
959 }
960 
961 
962 //
964 {
965  global $xoopsDB ;
966 
967  $ip = @$_SERVER['REMOTE_ADDR'] ;
968  $uri = @$_SERVER['REQUEST_URI'] ;
969  $ip4sql = addslashes( $ip ) ;
970  $uri4sql = addslashes( $uri ) ;
971  if( empty( $ip ) || $ip == '' ) return true ;
972 
973  $victim_uname = empty( $_COOKIE['autologin_uname'] ) ? $_POST['uname'] : $_COOKIE['autologin_uname'] ;
974  // some UA send 'deleted' as a value of the deleted cookie.
975  if( $victim_uname == 'deleted' ) return ;
976  $mal4sql = addslashes( "BRUTE FORCE: $victim_uname" ) ;
977 
978  // gargage collection
979  $result = $xoopsDB->queryF( "DELETE FROM ".$xoopsDB->prefix($this->mydirname."_access")." WHERE expire < UNIX_TIMESTAMP()" ) ;
980 
981  // sql for recording access log (INSERT should be placed after SELECT)
982  $sql4insertlog = "INSERT INTO ".$xoopsDB->prefix($this->mydirname."_access")." SET ip='$ip4sql',request_uri='$uri4sql',malicious_actions='$mal4sql',expire=UNIX_TIMESTAMP()+600" ;
983 
984  // count check
985  $result = $xoopsDB->query( "SELECT COUNT(*) FROM ".$xoopsDB->prefix($this->mydirname."_access")." WHERE ip='$ip4sql' AND malicious_actions like 'BRUTE FORCE:%'" ) ;
986  list( $bf_count ) = $xoopsDB->fetchRow( $result ) ;
987  if( $bf_count > $this->_conf['bf_count'] ) {
988  $this->register_bad_ips( time() + $this->_conf['banip_time0'] ) ;
989  $this->last_error_type = 'BruteForce' ;
990  $this->message .= "Trying to login as '".addslashes($victim_uname)."' found.\n" ;
991  $this->output_log( 'BRUTE FORCE' , 0 , true , 1 ) ;
992  $ret = $this->call_filter( 'bruteforce_overrun' ) ;
993  if( $ret == false ) exit ;
994  }
995  // delayed insert
996  $xoopsDB->queryF( $sql4insertlog ) ;
997 }
998 
999 
1001 {
1002  if( is_array( $val ) ) {
1003  foreach( $val as $subval ) {
1004  $this->_spam_check_point_recursive( $subval ) ;
1005  }
1006  } else {
1007  // http_host
1008  $path_array = parse_url( XOOPS_URL ) ;
1009  $http_host = empty( $path_array['host'] ) ? 'www.xoops.org' : $path_array['host'] ;
1010 
1011  // count URI up
1012  $count = -1 ;
1013  foreach( preg_split( '#https?\:\/\/#i' , $val ) as $fragment ) {
1014  if( strncmp( $fragment , $http_host , strlen( $http_host ) ) !== 0 ) {
1015  $count ++ ;
1016  }
1017  }
1018  if( $count > 0 ) $this->_spamcount_uri += $count ;
1019 
1020  // count BBCode likd [url=www....] up (without [url=http://...])
1021  $this->_spamcount_uri += count( preg_split( '/\[url=(?!http|\\"http|\\\'http|'.$http_host.')/i' , $val ) ) - 1 ;
1022  }
1023 }
1024 
1025 
1026 function spam_check( $points4deny , $uid )
1027 {
1028  $this->_spamcount_uri = 0 ;
1030 
1031  if( $this->_spamcount_uri >= $points4deny ) {
1032  $this->message .= @$_SERVER['REQUEST_URI']." SPAM POINT: $this->_spamcount_uri\n" ;
1033  $this->output_log( 'URI SPAM' , $uid , false , 128 ) ;
1034  $ret = $this->call_filter( 'spamcheck_overrun' ) ;
1035  if( $ret == false ) exit ;
1036  }
1037 }
1038 
1039 
1041 {
1042  global $HTTP_POST_VARS , $HTTP_GET_VARS , $HTTP_COOKIE_VARS ;
1043 
1044 
1045  // disable "Notice: Undefined index: ..."
1046  $error_reporting_level = error_reporting( 0 ) ;
1047 
1048 
1049  //
1050  // bit 1 : disable XMLRPC , criteria bug
1051  //
1052  if( $this->_conf['disable_features'] & 1 ) {
1053 
1054  // zx 2005/1/5 disable xmlrpc.php in root
1055  if( /* ! stristr( $_SERVER['SCRIPT_NAME'] , 'modules' ) && */ substr( @$_SERVER['SCRIPT_NAME'] , -10 ) == 'xmlrpc.php' ) {
1056  $this->output_log( 'xmlrpc' , 0 , true , 1 ) ;
1057  exit ;
1058  }
1059 
1060  // security bug of class/criteria.php 2005/6/27
1061  if( $_POST['uname'] === '0' || $_COOKIE['autologin_pass'] === '0' ) {
1062  $this->output_log( 'CRITERIA' ) ;
1063  exit ;
1064  }
1065  }
1066 
1067 
1068  //
1069  // bit 11 : XSS+CSRFs in XOOPS < 2.0.10
1070  //
1071  if( $this->_conf['disable_features'] & 1024 ) {
1072 
1073  // root controllers
1074  if( ! stristr( @$_SERVER['SCRIPT_NAME'] , 'modules' ) ) {
1075  // zx 2004/12/13 misc.php debug (file check)
1076  if( substr( @$_SERVER['SCRIPT_NAME'] , -8 ) == 'misc.php' && ( $_GET['type'] == 'debug' || $_POST['type'] == 'debug' ) && ! preg_match( '/^dummy_[0-9]+\.html$/' , $_GET['file'] ) ) {
1077  $this->output_log( 'misc debug' ) ;
1078  exit ;
1079  }
1080 
1081  // zx 2004/12/13 misc.php smilies
1082  if( substr( @$_SERVER['SCRIPT_NAME'] , -8 ) == 'misc.php' && ( $_GET['type'] == 'smilies' || $_POST['type'] == 'smilies' ) && ! preg_match( '/^[0-9a-z_]*$/i' , $_GET['target'] ) ) {
1083  $this->output_log( 'misc smilies' ) ;
1084  exit ;
1085  }
1086 
1087  // zx 2005/1/5 edituser.php avatarchoose
1088  if( substr( @$_SERVER['SCRIPT_NAME'] , -12 ) == 'edituser.php' && $_POST['op'] == 'avatarchoose' && strstr( $_POST['user_avatar'] , '..' ) ) {
1089  $this->output_log( 'edituser avatarchoose' ) ;
1090  exit ;
1091  }
1092  }
1093 
1094  // zx 2005/1/4 findusers
1095  if( substr( @$_SERVER['SCRIPT_NAME'] , -24 ) == 'modules/system/admin.php' && ( $_GET['fct'] == 'findusers' || $_POST['fct'] == 'findusers' ) ) {
1096  foreach( $_POST as $key => $val ) {
1097  if( strstr( $key , "'" ) || strstr( $val , "'" ) ) {
1098  $this->output_log( 'findusers' ) ;
1099  exit ;
1100  }
1101  }
1102  }
1103 
1104  // preview CSRF zx 2004/12/14
1105  // news submit.php
1106  if( substr( @$_SERVER['SCRIPT_NAME'] , -23 ) == 'modules/news/submit.php' && isset( $_POST['preview'] ) && strpos( @$_SERVER['HTTP_REFERER'] , XOOPS_URL.'/modules/news/submit.php' ) !== 0 ) {
1107  $HTTP_POST_VARS['nohtml'] = $_POST['nohtml'] = 1 ;
1108  }
1109  // news admin/index.php
1110  if( substr( @$_SERVER['SCRIPT_NAME'] , -28 ) == 'modules/news/admin/index.php' && ( $_POST['op'] == 'preview' || $_GET['op'] == 'preview' ) && strpos( @$_SERVER['HTTP_REFERER'] , XOOPS_URL.'/modules/news/admin/index.php' ) !== 0 ) {
1111  $HTTP_POST_VARS['nohtml'] = $_POST['nohtml'] = 1 ;
1112  }
1113  // comment comment_post.php
1114  if( isset( $_POST['com_dopreview'] ) && ! strstr( substr( @$_SERVER['HTTP_REFERER'] , -16 ) , 'comment_post.php' ) ) {
1115  $HTTP_POST_VARS['dohtml'] = $_POST['dohtml'] = 0 ;
1116  }
1117  // disable preview of system's blocksadmin
1118  if( substr( @$_SERVER['SCRIPT_NAME'] , -24 ) == 'modules/system/admin.php' && ( $_GET['fct'] == 'blocksadmin' || $_POST['fct'] == 'blocksadmin') && isset( $_POST['previewblock'] ) /* && strpos( $_SERVER['HTTP_REFERER'] , XOOPS_URL.'/modules/system/admin.php' ) !== 0 */ ) {
1119  die( "Danger! don't use this preview. Use 'altsys module' instead.(by Protector)" ) ;
1120  }
1121  // tpl preview
1122  if( substr( @$_SERVER['SCRIPT_NAME'] , -24 ) == 'modules/system/admin.php' && ( $_GET['fct'] == 'tplsets' || $_POST['fct'] == 'tplsets') ) {
1123  if( $_POST['op'] == 'previewpopup' || $_GET['op'] == 'previewpopup' || isset( $_POST['previewtpl'] ) ) {
1124  die( "Danger! don't use this preview.(by Protector)" ) ;
1125  }
1126  }
1127 
1128  }
1129 
1130 
1131  // restore reporting level
1132  error_reporting( $error_reporting_level ) ;
1133 }
1134 
1135 
1136 function call_filter( $type , $dying_message = '' )
1137 {
1138  require_once dirname(__FILE__).'/ProtectorFilter.php' ;
1139  $filter_handler =& ProtectorFilterHandler::getInstance() ;
1140  $ret = $filter_handler->execute( $type ) ;
1141  if( $ret == false && $dying_message ) {
1142  die( $dying_message ) ;
1143  }
1144 
1145  return $ret ;
1146 }
1147 
1148 
1149 
1150 }
1151 ?>