11<?php
22/*
33Plugin Name: Block Bad Requests (required from wp-config or MU plugin)
4- Version: 2.20 .0
4+ Version: 2.21 .0
55Description: Stop various HTTP attacks and trigger Fail2ban.
66Plugin URI: https://github.com/szepeviktor/wordpress-fail2ban
77License: The MIT License (MIT)
3838 */
3939final class Bad_Request {
4040
41- private $ prefix = 'Malicious traffic detected: ' ;
42- private $ prefix_instant = 'Break-in attempt detected: ' ;
41+ private $ prefix = 'Malicious traffic detected: ' ;
42+ private $ prefix_instant = 'Break-in attempt detected: ' ;
4343 private $ instant_trigger = true ;
4444 // Default rest_url_prefix value
45- private $ rest_url_prefix = '/wp-json/ ' ;
45+ private $ rest_url_prefix = '/wp-json/ ' ;
4646 private $ max_login_request_size = 4000 ;
47- private $ is_wplogin = false ;
48- private $ is_xmlrpc = false ;
49- private $ is_rest = false ;
50- private $ is_options_method = false ;
51- private $ names2ban = array (
47+ private $ is_wplogin = false ;
48+ private $ is_xmlrpc = false ;
49+ private $ is_rest = false ;
50+ private $ is_options_method = false ;
51+ private $ names2ban = array (
5252 'access ' ,
5353 'admin ' ,
5454 'administrator ' ,
@@ -84,7 +84,7 @@ final class Bad_Request {
8484 'username ' ,
8585 'webmaster ' ,
8686 );
87- private $ blacklist = array (
87+ private $ blacklist = array (
8888 '../ ' , // Path traversal
8989 '/.. ' , // Path traversal
9090 'wp-config ' , // WP configuration
@@ -101,7 +101,7 @@ final class Bad_Request {
101101 '/administrator ' , // Joomla Administrator
102102 'connector.asp ' , // Joomla FCKeditor 2.x File Manager Connector for ASP
103103 '/HNAP1 ' , // D-Link routers
104- '() { ' , // Shell shock () { :;};
104+ '() { ' , // Shell shock, Bash script: () { :;};
105105 '/cgi-bin/ ' , // CGI folder
106106 'error_log ' , // Default PHP error log
107107 'error-log ' , // PHP error log
@@ -117,26 +117,39 @@ final class Bad_Request {
117117 '+--+ ' , // SQL comment
118118 '%20--%20 ' , // SQL comment
119119 );
120- private $ relative_request_uri = '' ;
120+ private $ relative_request_uri = '' ;
121121 private $ cdn_headers ;
122- private $ allow_registration = false ;
123- private $ allow_ie8_login = false ;
124- private $ allow_old_proxies = false ;
122+ private $ allow_registration = false ;
123+ private $ allow_ie8_login = false ;
124+ private $ allow_old_proxies = false ;
125125 private $ allow_connection_empty = false ;
126126 private $ allow_connection_close = false ;
127- private $ allow_two_capitals = false ;
128- private $ disallow_tor_login = false ;
129- private $ result = false ;
127+ private $ allow_two_capitals = false ;
128+ private $ disallow_tor_login = false ;
129+ private $ result = false ;
130130
131131 /**
132132 * Set up options, run check and trigger fail2ban on malicous HTTP request.
133133 */
134134 public function __construct () {
135135
136+ if ( empty ( $ _SERVER ['SERVER_ADDR ' ] )
137+ || empty ( $ _SERVER ['REMOTE_ADDR ' ] )
138+ || empty ( $ _SERVER ['REMOTE_PORT ' ] )
139+ || empty ( $ _SERVER ['REQUEST_METHOD ' ] )
140+ || empty ( $ _SERVER ['REQUEST_URI ' ] )
141+ ) {
142+ $ this ->prefix = 'Server configuration error: ' ;
143+ $ this ->instant_trigger = false ;
144+ $ this ->result = 'bad_request_superglobal ' ;
145+ $ this ->trigger ();
146+ exit ;
147+ }
148+
136149 // Don't run on local access and on install or upgrade
137150 // WP_INSTALLING is available even before wp-config.php
138151 if ( 'cli ' === php_sapi_name ()
139- || $ _SERVER ['REMOTE_ADDR ' ] === $ _SERVER ['SERVER_ADDR ' ]
152+ || $ _SERVER ['REMOTE_ADDR ' ] === $ _SERVER ['SERVER_ADDR ' ] // WPCS: input var okay.
140153 || ( defined ( 'WP_INSTALLING ' ) && WP_INSTALLING )
141154 ) {
142155 return ;
@@ -147,18 +160,19 @@ public function __construct() {
147160 $ this ->instant_trigger = false ;
148161 }
149162
150- if ( isset ( $ _SERVER ['REQUEST_URI ' ] ) ) {
151- $ this ->relative_request_uri = $ _SERVER ['REQUEST_URI ' ];
152- }
163+ $ this ->relative_request_uri = $ _SERVER ['REQUEST_URI ' ];
153164 // O1_BAD_REQUEST_PROXY_HOME_URL should not contain a trailing slash
154165 if ( defined ( 'O1_BAD_REQUEST_PROXY_HOME_URL ' ) ) {
155166 $ home_url_length = strlen ( O1_BAD_REQUEST_PROXY_HOME_URL );
156167 if ( O1_BAD_REQUEST_PROXY_HOME_URL === substr ( $ _SERVER ['REQUEST_URI ' ], 0 , $ home_url_length ) ) {
157168 $ this ->relative_request_uri = substr ( $ _SERVER ['REQUEST_URI ' ], $ home_url_length );
158- // Fix request URI
159- // https://core.trac.wordpress.org/ticket/39586
160- // Uncomment next line
161- //$_SERVER['REQUEST_URI'] = $this->relative_request_uri;
169+ /**
170+ * Fix request URI
171+ *
172+ * @see https://core.trac.wordpress.org/ticket/39586
173+ *
174+ * $_SERVER['REQUEST_URI'] = $this->relative_request_uri;
175+ */
162176 }
163177 }
164178
@@ -243,14 +257,14 @@ function apache_request_headers() {
243257 }
244258
245259 // Request methods
246- $ request_method = strtoupper ( $ _SERVER ['REQUEST_METHOD ' ] );
247- $ wp_methods = array ( 'HEAD ' , 'GET ' , 'POST ' );
260+ $ request_method = strtoupper ( $ _SERVER ['REQUEST_METHOD ' ] );
261+ $ wp_methods = array ( 'HEAD ' , 'GET ' , 'POST ' );
248262 $ wp_login_methods = array ( 'GET ' , 'POST ' );
249- $ wp_rest_methods = array ( 'GET ' , 'HEAD ' , 'POST ' , 'PUT ' , 'DELETE ' , 'OPTIONS ' );
250- $ write_methods = array ( 'POST ' , 'PUT ' , 'DELETE ' );
263+ $ wp_rest_methods = array ( 'GET ' , 'HEAD ' , 'POST ' , 'PUT ' , 'DELETE ' , 'OPTIONS ' );
264+ $ write_methods = array ( 'POST ' , 'PUT ' , 'DELETE ' );
251265
252266 // Dissect request URI
253- $ request_path = parse_url ( $ this ->relative_request_uri , PHP_URL_PATH );
267+ $ request_path = parse_url ( $ this ->relative_request_uri , PHP_URL_PATH );
254268 $ request_query = isset ( $ _SERVER ['QUERY_STRING ' ] )
255269 ? $ _SERVER ['QUERY_STRING ' ]
256270 : parse_url ( $ this ->relative_request_uri , PHP_URL_QUERY );
@@ -282,7 +296,7 @@ function apache_request_headers() {
282296 );
283297 $ this ->enhanced_error_log ( 'HTTP headers: ' . $ this ->esc_log ( $ cdn_combined_headers ) );
284298 // Work-around to prevent edge server banning
285- $ this ->prefix = 'Attack through CDN: ' ;
299+ $ this ->prefix = 'Attack through CDN: ' ;
286300 $ this ->instant_trigger = false ;
287301 return 'bad_request_cdn_attack ' ;
288302 }
@@ -311,18 +325,18 @@ function apache_request_headers() {
311325 // https://tools.ietf.org/html/rfc2616#section-9.2
312326 if ( ! $ this ->is_rest && 'OPTIONS ' === $ request_method ) {
313327 $ this ->is_options_method = true ;
314- $ this ->instant_trigger = false ;
328+ $ this ->instant_trigger = false ;
315329 return 'bad_request_http_options_method ' ;
316330 }
317331 if ( ! $ this ->is_wplogin && ! $ this ->is_rest
318- && false === in_array ( $ request_method , $ wp_methods )
332+ && false === in_array ( $ request_method , $ wp_methods, true )
319333 ) {
320334 return 'bad_request_http_method ' ;
321335 }
322- if ( $ this ->is_wplogin && false === in_array ( $ request_method , $ wp_login_methods ) ) {
336+ if ( $ this ->is_wplogin && false === in_array ( $ request_method , $ wp_login_methods, true ) ) {
323337 return 'bad_request_wplogin_http_method ' ;
324338 }
325- if ( $ this ->is_rest && false === in_array ( $ request_method , $ wp_rest_methods ) ) {
339+ if ( $ this ->is_rest && false === in_array ( $ request_method , $ wp_rest_methods, true ) ) {
326340 return 'bad_request_rest_http_method ' ;
327341 }
328342
@@ -347,9 +361,11 @@ function apache_request_headers() {
347361 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
348362 // "#" removed
349363 // "%" added
364+ // Look for "http:" in request path
350365 if ( substr_count ( $ _SERVER ['REQUEST_URI ' ], '? ' ) > 1
351366 || false !== strpos ( $ _SERVER ['REQUEST_URI ' ], '# ' )
352367 || 1 === preg_match ( "/[^%:\/?\[\]@!$&'()*+,;=A-Za-z0-9._~-]/ " , $ _SERVER ['REQUEST_URI ' ] )
368+ || 1 === preg_match ( '/(http|https|data):/i ' , $ request_path )
353369 ) {
354370 $ this ->instant_trigger = false ;
355371 return 'bad_request_uri_encoding ' ;
@@ -369,7 +385,11 @@ function apache_request_headers() {
369385 // http://httpd.apache.org/docs/current/custom-error.html#variables
370386 if ( isset ( $ _SERVER ['REDIRECT_URL ' ] )
371387 && false !== stripos ( $ _SERVER ['REDIRECT_URL ' ], '.php ' )
372- // For old mod_fastcgi setup && $_SERVER['SCRIPT_NAME'] !== $_SERVER['REDIRECT_URL']
388+ /**
389+ * For old mod_fastcgi setup
390+ *
391+ * && $_SERVER['SCRIPT_NAME'] !== $_SERVER['REDIRECT_URL']
392+ */
373393 ) {
374394 return 'bad_request_nonexistent_php ' ;
375395 }
@@ -391,7 +411,7 @@ function apache_request_headers() {
391411 }
392412
393413 // Check write-type method requests only
394- if ( false === in_array ( $ request_method , $ write_methods ) ) {
414+ if ( false === in_array ( $ request_method , $ write_methods, true ) ) {
395415 // Not a write-type method
396416 return false ;
397417 }
@@ -523,7 +543,7 @@ function apache_request_headers() {
523543 $ username = trim ( $ _POST ['log ' ] );
524544
525545 // Banned usernames
526- if ( in_array ( strtolower ( $ username ), $ this ->names2ban ) ) {
546+ if ( in_array ( strtolower ( $ username ), $ this ->names2ban , true ) ) {
527547 return 'bad_request_wplogin_username_banned ' ;
528548 }
529549
@@ -615,9 +635,9 @@ function apache_request_headers() {
615635
616636 // Tor network exit node detection
617637 if ( $ this ->disallow_tor_login ) {
618- $ exitlist_tpl = '%s.80.%s.ip-port.exitlist.torproject.org ' ;
619- $ remote_rev = implode ( '. ' , array_reverse ( explode ( '. ' , $ _SERVER ['REMOTE_ADDR ' ] ) ) );
620- $ server_rev = implode ( '. ' , array_reverse ( explode ( '. ' , $ _SERVER ['SERVER_ADDR ' ] ) ) );
638+ $ exitlist_tpl = '%s.80.%s.ip-port.exitlist.torproject.org ' ;
639+ $ remote_rev = implode ( '. ' , array_reverse ( explode ( '. ' , $ _SERVER ['REMOTE_ADDR ' ] ) ) );
640+ $ server_rev = implode ( '. ' , array_reverse ( explode ( '. ' , $ _SERVER ['SERVER_ADDR ' ] ) ) );
621641 $ exitlist_response = gethostbyname ( sprintf ( $ exitlist_tpl , $ remote_rev , $ server_rev ) );
622642 if ( false !== strpos ( $ exitlist_response , '127.0.0 ' ) ) {
623643 return 'bad_request_wplogin_tor ' ;
@@ -636,6 +656,7 @@ private function trigger() {
636656 // Trigger miniban
637657 if ( class_exists ( '\Miniban ' ) && $ this ->instant_trigger ) {
638658 if ( true !== \Miniban ::ban () ) {
659+ // @codingStandardsChangeSetting WordPress.PHP.DevelopmentFunctions exclude error_log
639660 error_log ( 'Miniban operation failed. ' );
640661 }
641662 }
@@ -692,15 +713,15 @@ private function disable_options_method() {
692713
693714 private function fake_wplogin () {
694715
695- $ server_name = isset ( $ _SERVER ['SERVER_NAME ' ] )
716+ $ server_name = isset ( $ _SERVER ['SERVER_NAME ' ] )
696717 ? $ _SERVER ['SERVER_NAME ' ]
697718 : $ _SERVER ['HTTP_HOST ' ];
698- $ username = trim ( $ _POST ['log ' ] );
699- $ expire = time () + 3600 ;
700- $ token = substr ( hash_hmac ( 'sha256 ' , rand (), 'token ' ), 0 , 43 );
701- $ hash = hash_hmac ( 'sha256 ' , rand (), 'hash ' );
702- $ auth_cookie = $ username . '| ' . $ expire . '| ' . $ token . '| ' . $ hash ;
703- $ authcookie_name = 'wordpress_ ' . md5 ( 'authcookie ' );
719+ $ username = trim ( $ _POST ['log ' ] );
720+ $ expire = time () + 3600 ;
721+ $ token = substr ( hash_hmac ( 'sha256 ' , rand (), 'token ' ), 0 , 43 );
722+ $ hash = hash_hmac ( 'sha256 ' , rand (), 'hash ' );
723+ $ auth_cookie = $ username . '| ' . $ expire . '| ' . $ token . '| ' . $ hash ;
724+ $ authcookie_name = 'wordpress_ ' . md5 ( 'authcookie ' );
704725 $ loggedincookie_name = 'wordpress_logged_in_ ' . md5 ( 'cookiehash ' );
705726
706727 header ( 'Cache-Control: max-age=0, private, no-store, no-cache, must-revalidate ' );
@@ -744,7 +765,7 @@ private function fake_xmlrpc() {
744765 ' ,
745766 $ server_name ,
746767 $ server_name
747- );
768+ ); // WPCS: XSS ok.
748769 }
749770
750771 /**
@@ -764,9 +785,9 @@ private function enhanced_error_log( $message = '', $level = 'error' ) {
764785 */
765786
766787 // Add entry point, correct when `auto_prepend_file` is empty
767- $ included_files = get_included_files ();
788+ $ included_files = get_included_files ();
768789 $ first_included_file = reset ( $ included_files );
769- $ error_msg = sprintf ( '%s <%s ' ,
790+ $ error_msg = sprintf ( '%s <%s ' ,
770791 $ message ,
771792 $ this ->esc_log ( sprintf ( '%s:%s ' , $ _SERVER ['REQUEST_METHOD ' ], $ first_included_file ) )
772793 );
@@ -792,6 +813,7 @@ private function enhanced_error_log( $message = '', $level = 'error' ) {
792813 );
793814 }
794815
816+ // @codingStandardsChangeSetting WordPress.PHP.DevelopmentFunctions exclude error_log
795817 error_log ( $ error_msg );
796818 }
797819
@@ -804,7 +826,7 @@ private function enhanced_error_log( $message = '', $level = 'error' ) {
804826 */
805827 private function parse_query ( $ query_string ) {
806828
807- $ query = array ();
829+ $ query = array ();
808830 $ names_values_array = explode ( '& ' , $ query_string );
809831
810832 foreach ( $ names_values_array as $ name_value ) {
@@ -831,7 +853,7 @@ private function parse_query( $query_string ) {
831853 */
832854 private function esc_log ( $ string ) {
833855
834- $ escaped = serialize ( $ string );
856+ $ escaped = json_encode ( $ string );
835857 // Limit length
836858 $ escaped = mb_substr ( $ escaped , 0 , 500 , 'utf-8 ' );
837859 // New lines to "|"
@@ -876,14 +898,15 @@ private function get_leafs( $array ) {
876898 return $ leafs ;
877899 }
878900
879- $ array_iterator = new \RecursiveArrayIterator ( $ array );
901+ $ array_iterator = new \RecursiveArrayIterator ( $ array );
880902 $ iterator_iterator = new \RecursiveIteratorIterator ( $ array_iterator , \RecursiveIteratorIterator::LEAVES_ONLY );
881903 foreach ( $ iterator_iterator as $ key => $ value ) {
882- $ keys = array ();
883- for ( $ i = 0 ; $ i < $ iterator_iterator ->getDepth (); $ i ++ ) {
904+ $ keys = array ();
905+ $ depth = $ iterator_iterator ->getDepth ();
906+ for ( $ i = 0 ; $ i < $ depth ; $ i ++ ) {
884907 $ keys [] = $ iterator_iterator ->getSubIterator ( $ i )->key ();
885908 }
886- $ keys [] = $ key ;
909+ $ keys [] = $ key ;
887910 $ leaf_key = implode ( ' ' , $ keys );
888911
889912 $ leafs [ $ leaf_key ] = $ value ;
@@ -921,13 +944,13 @@ private function is_ie( $ua ) {
921944 private function rebuild_query ( $ request_query ) {
922945
923946 $ rebuilt_query = array ();
924- $ query_lenght = strlen ( $ request_query );
925- $ queries = $ this ->parse_query ( $ request_query );
947+ $ query_lenght = strlen ( $ request_query );
948+ $ queries = $ this ->parse_query ( $ request_query );
926949
927950 foreach ( $ queries as $ key => $ value ) {
928951 $ rebuilt_query [] = sprintf ( '%s=%s ' ,
929- urlencode ( urldecode ( $ key ) ),
930- urlencode ( urldecode ( $ value ) )
952+ rawurlencode ( urldecode ( $ key ) ),
953+ rawurlencode ( urldecode ( $ value ) )
931954 );
932955 }
933956
0 commit comments