PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.8
Patchstack – WordPress & Plugins Security v2.1.8
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
← All changes | includes/firewall.php +746 -217 trunk2.1.8 View file →
@@ -1,217 +1,746 @@
1 -<?php
2 -
3 -// Do not allow the file to be called directly.
4 -if ( ! defined( 'ABSPATH' ) ) {
5 - exit;
6 -}
7 -
8 -/**
9 - * This class provides the firewall functionality.
10 - */
11 -class P_Firewall extends P_Core {
12 -
13 - /**
14 - * Launch the firewall rule processor.
15 - *
16 - * @param bool $from_main Whether or not the firewall is loaded from the main script or not.
17 - * @param Patchstack $core
18 - * @param bool $skip Whether or not to process and execute the rules.
19 - * @param bool $muCall Whether or not this was called from mu-plugin.
20 - * @return void
21 - */
22 - public function __construct( $from_main = false, $core = null, $skip = false, $muCall = false ) {
23 - if ( ! $from_main || ! $core ) {
24 - if ( $core ) {
25 - parent::__construct( $core );
26 - }
27 - return;
28 - }
29 -
30 - parent::__construct( $core );
31 -
32 - // If we only want to initialize the firewall but not execute the rules.
33 - if ( $skip || defined( 'DOING_CRON' ) ) {
34 - return;
35 - }
36 -
37 - // Load the extension.
38 - require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php';
39 - $extension = new Patchstack\Extensions\WordPress\Extension(
40 - [
41 - 'patchstack_basic_firewall_roles' => $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] ),
42 - 'patchstack_whitelist' => get_option( 'patchstack_whitelist', '' )
43 - ],
44 - $this
45 - );
46 -
47 - // Initiate the firewall processor with our settings.
48 - $firewall = new Patchstack\Processor(
49 - $extension,
50 - $this->decode_rule_option('patchstack_firewall_rules_v3'),
51 - $this->decode_rule_option('patchstack_whitelist_rules_v3'),
52 - [
53 - 'autoblockAttempts' => $this->get_option( 'patchstack_autoblock_attempts', 10 ),
54 - 'autoblockMinutes' => $this->get_option( 'patchstack_autoblock_minutes', 30 ),
55 - 'autoblockTime' => $this->get_option( 'patchstack_autoblock_blocktime', 60 ),
56 - 'whitelistKeysRules' => $this->decode_rule_option( 'patchstack_whitelist_keys_rules' ),
57 - 'mustUsePluginCall' => $muCall
58 - ],
59 - $this->decode_rule_option('patchstack_firewall_rules'),
60 - $this->decode_rule_option('patchstack_whitelist_rules')
61 - );
62 -
63 - // Launch the firewall.
64 - $firewall->launch();
65 - }
66 -
67 - /**
68 - * Safely decode a stored rule option into an array. The option is expected to be
69 - * a JSON string, but may already be an array (or a malformed value); passing a
70 - * non-string to json_decode() is a fatal TypeError on PHP 8.
71 - *
72 - * @param string $name
73 - * @return array
74 - */
75 - private function decode_rule_option( $name ) {
76 - $value = get_option( $name, '[]' );
77 -
78 - if ( is_array( $value ) ) {
79 - return $value;
80 - }
81 -
82 - if ( ! is_string( $value ) ) {
83 - return [];
84 - }
85 -
86 - $decoded = json_decode( $value, true );
87 - return is_array( $decoded ) ? $decoded : [];
88 - }
89 -
90 - /**
91 - * Determine if the user is authenticated and in the list of whitelisted roles.
92 - *
93 - * @return bool
94 - */
95 - public function is_authenticated() {
96 - if ( ! is_user_logged_in() ) {
97 - return false;
98 - }
99 -
100 - // Get the whitelisted roles.
101 - $roles = $this->get_option( 'patchstack_basic_firewall_roles', [ 'administrator', 'editor', 'author' ] );
102 - if ( ! is_array( $roles ) ) {
103 - return false;
104 - }
105 -
106 - // Special scenario for super admins on a multisite environment.
107 - if ( in_array( 'administrator', $roles ) && is_multisite() && is_super_admin() ) {
108 - return true;
109 - }
110 -
111 - // Get the roles of the user.
112 - $user = wp_get_current_user();
113 - if ( ! isset( $user->roles ) || count( (array) $user->roles ) == 0 ) {
114 - return false;
115 - }
116 -
117 - // Is the user in the whitelist roles list?
118 - $role_count = array_intersect( $user->roles, $roles );
119 - return count( $role_count ) != 0;
120 - }
121 -
122 - /**
123 - * Display error page.
124 - *
125 - * @param integer $fid
126 - * @return void
127 - */
128 - public function display_error_page( $fid = 1 ) {
129 - if ( $fid != 22 && $fid != 23 && $fid != 24 && $fid != 'login' ) {
130 - $this->log_request( $fid );
131 - }
132 -
133 - // Supported by a number of popular caching plugins.
134 - if ( ! defined( 'DONOTCACHEPAGE' ) ) {
135 - define( 'DONOTCACHEPAGE', true );
136 - }
137 -
138 - // Because WP Fastest Cache just has to be special...
139 - if (function_exists('wpfc_exclude_current_page')) {
140 - @wpfc_exclude_current_page();
141 - }
142 -
143 - // Send forbidden headers and no-caching headers as well.
144 - status_header(403);
145 - send_nosniff_header();
146 - nocache_headers();
147 -
148 - if ( $fid == 'login' ) {
149 - require_once dirname( __FILE__ ) . '/views/access-denied-login.php';
150 - } else {
151 - require_once dirname( __FILE__ ) . '/views/access-denied.php';
152 - }
153 -
154 - exit;
155 - }
156 -
157 - /**
158 - * Log the blocked request.
159 - *
160 - * @param int $fid
161 - * @return void
162 - */
163 - private function log_request( $fid = 1 ) {
164 - global $wpdb;
165 - if ( ! $wpdb || $fid == 22 || $fid == 23 || $fid == 24 || $fid == 'login' ) {
166 - return;
167 - }
168 -
169 - // Insert into the logs.
170 - $wpdb->insert(
171 - $wpdb->prefix . 'patchstack_firewall_log',
172 - array(
173 - 'ip' => $this->get_ip(),
174 - 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '',
175 - 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
176 - 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '',
177 - 'fid' => $fid,
178 - 'flag' => '',
179 - 'post_data' => '',
180 - 'block_type' => 'BLOCK',
181 - )
182 - );
183 - }
184 -
185 - /**
186 - * Extract the number of blocked hits the past 30 days based on the current counters.
187 - *
188 - * @return int
189 - */
190 - public function get_hits_counter() {
191 - $counters = get_option( 'patchstack_hits_last_30', [] );
192 - if (!is_array($counters) || count($counters) === 0) {
193 - return 0;
194 - }
195 -
196 - // Set the range of dates we need.
197 - $hits = 0;
198 - $start = new \DateTime();
199 - $start->modify('-30 days');
200 -
201 - $end = new \DateTime();
202 - $end->modify('+1 day');
203 -
204 - $interval = new \DateInterval('P1D');
205 - $range = new \DatePeriod($start, $interval, $end);
206 -
207 - // Set the range from -6 days to +1 day from now.
208 - foreach ($range as $date) {
209 - $formattedDate = $date->format('Y-m-d');
210 - if (isset($counters[$formattedDate])) {
211 - $hits += $counters[$formattedDate];
212 - }
213 - }
214 -
215 - return $hits;
216 - }
217 -}
1 +<?php
2 +
3 +// Do not allow the file to be called directly.
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
7 +
8 +/**
9 + * This class provides the firewall functionality.
10 + */
11 +class P_Firewall extends P_Core {
12 +
13 + /**
14 + * Parse the firewall and whitelist rules and determine if it's valid.
15 + * Then set the types with all server/client variables and launch the processor.
16 + *
17 + * @param bool $from_main Whether or not the firewall is loaded from the main script or not.
18 + * @param Patchstack $core
19 + * @param bool $skip Whether or not to process and execute the rules.
20 + * @return void
21 + */
22 + public function __construct( $from_main = false, $core = null, $skip = false ) {
23 + if ( ! $from_main || ! $core ) {
24 + if ( $core ) {
25 + parent::__construct( $core );
26 + }
27 + return;
28 + }
29 +
30 + parent::__construct( $core );
31 +
32 + // If we only want to initialize the firewall but not execute the rules.
33 + if ( $skip ) {
34 + return;
35 + }
36 +
37 + // Process the firewall rules.
38 + $this->processor();
39 + }
40 +
41 + /**
42 + * Check the custom whitelist rules defined in the backend of WordPress
43 + * and attempt to match it with the request.
44 + *
45 + * @return boolean
46 + */
47 + private function is_custom_whitelisted() {
48 + $whitelist = str_replace( '<?php exit; ?>', '', get_option( 'patchstack_custom_whitelist_rules', '' ) );
49 + if ( empty( $whitelist ) ) {
50 + return false;
51 + }
52 +
53 + // Loop through all lines.
54 + $lines = explode( "\n", $whitelist );
55 + $ip = $this->get_ip();
56 +
57 + foreach ( $lines as $line ) {
58 + $t = explode( ':', $line );
59 +
60 + if ( count( $t ) == 2 ) {
61 + $val = strtolower( trim( $t[1] ) );
62 + switch ( strtolower( $t[0] ) ) {
63 + // IP address match.
64 + case 'ip':
65 + if ( $ip == $val ) {
66 + return true;
67 + }
68 + break;
69 + // Payload match.
70 + case 'payload':
71 + if ( count( $_POST ) > 0 && strpos( strtolower( print_r( $_POST, true ) ), $val ) !== false ) {
72 + return true;
73 + }
74 +
75 + if ( count( $_GET ) > 0 && strpos( strtolower( print_r( $_GET, true ) ), $val ) !== false ) {
76 + return true;
77 + }
78 + break;
79 + // URL match.
80 + case 'url':
81 + if ( strpos( strtolower( $_SERVER['REQUEST_URI'] ), $val ) !== false ) {
82 + return true;
83 + }
84 + break;
85 + }
86 + }
87 + }
88 +
89 + return false;
90 + }
91 +
92 + /**
93 + * Determine if the request should be whitelisted.
94 + *
95 + * @return boolean
96 + */
97 + private function is_whitelisted() {
98 + // First check if the user has custom whitelist rules configured.
99 + if ( $this->is_custom_whitelisted() ) {
100 + return true;
101 + }
102 +
103 + // Load the whitelist.
104 + $whitelists = get_option( 'patchstack_whitelist_rules', '' );
105 + if ( $whitelists == null || $whitelists == '' ) {
106 + return false;
107 + }
108 +
109 + // Parse the whitelist.
110 + $whitelists = json_decode( str_replace( '<?php exit; ?>', '', $whitelists ), true );
111 +
112 + // Grab visitor's IP address and request data.
113 + $client_ip = $this->get_ip();
114 + $requests = $this->capture_request();
115 +
116 + foreach ( $whitelists as $whitelist ) {
117 + $whitelist_rule = json_decode( $whitelist['rule'] );
118 + $matched_rules = 0;
119 +
120 + // If matches on all request methods, only 1 rule match is required to whitelist.
121 + if ( $whitelist_rule->method === 'ALL' ) {
122 + $count_rules = 1;
123 + } else {
124 + if ( ! is_null( $whitelist_rule ) ) {
125 + $count_rules = $whitelist_rule->rules;
126 + $count_rules = $this->count_rules( $count_rules );
127 + }
128 + }
129 +
130 + // If an IP address match is given, determine if it matches.
131 + $ip = isset( $whitelist_rule->rules, $whitelist_rule->rules->ip_address ) ? $whitelist_rule->rules->ip_address : null;
132 + if ( ! is_null( $ip ) ) {
133 + if ( strpos( $ip, '*' ) !== false ) {
134 + $whitelisted_ip = $this->plugin->ban->check_wildcard_rule( $client_ip, $ip );
135 + } elseif ( strpos( $ip, '-' ) !== false ) {
136 + $whitelisted_ip = $this->plugin->ban->check_range_rule( $client_ip, $ip );
137 + } elseif ( strpos( $ip, '/' ) !== false ) {
138 + $whitelisted_ip = $this->plugin->ban->check_subnet_mask_rule( $client_ip, $ip );
139 + } elseif ( $client_ip == $ip ) {
140 + $whitelisted_ip = true;
141 + } else {
142 + $whitelisted_ip = false;
143 + }
144 + } else {
145 + $whitelisted_ip = true;
146 + }
147 +
148 + foreach ( $requests as $key => $request ) {
149 +
150 + // Treat the raw POST data string as the body contents of all values combined.
151 + if ( $key == 'rulesRawPost' ) {
152 + $key = 'rulesBodyAll';
153 + }
154 +
155 + if ( $whitelist_rule->method == $requests['method'] || $whitelist_rule->method == 'ALL' ) {
156 + $test = strtolower( preg_replace( '/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key ) );
157 + $rule = array_reduce(
158 + explode( '->', $test ),
159 + function ( $o, $p ) {
160 + if ( ! isset( $o->$p ) ) {
161 + return null;
162 + }
163 +
164 + return $o->$p;
165 + },
166 + $whitelist_rule
167 + );
168 +
169 + if ( ! is_null( $rule ) && substr( $key, 0, 4 ) == 'rule' && $this->is_rule_match( $rule, $request ) ) {
170 + $matched_rules++;
171 + }
172 + }
173 + }
174 +
175 + if ( $matched_rules >= $count_rules && $whitelisted_ip ) {
176 + return true;
177 + }
178 + }
179 +
180 + return false;
181 + }
182 +
183 + /**
184 + * Retrieve all HTTP headers that start with HTTP_.
185 + *
186 + * @return array
187 + */
188 + private function get_headers() {
189 + $headers = array();
190 + foreach ( $_SERVER as $name => $value ) {
191 + if ( substr( $name, 0, 5 ) == 'HTTP_' ) {
192 + $headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value;
193 + }
194 + }
195 +
196 + return $headers;
197 + }
198 +
199 + /**
200 + * Retrieve information about any file uploads.
201 + *
202 + * @return array
203 + */
204 + private function get_file_upload_data() {
205 + if ( ! is_array( $_FILES ) || count( $_FILES ) == 0 ) {
206 + return '';
207 + }
208 +
209 + // Extract the information we need from $_FILES.
210 + $return = array();
211 + foreach ( $_FILES as $key => $data ) {
212 + foreach ( $data as $key2 => $data2 ) {
213 +
214 + // We only want the name and type.
215 + if ( ! in_array( $key2, array( 'name', 'type' ) ) ) {
216 + continue;
217 + }
218 +
219 + if ( ! is_array( $data2 ) ) {
220 + $return[] = $key2 . '=' . $data2;
221 + } else {
222 + $return[] = $key2 . '=' . @$this->multi_implode( $data2, '&' . $key2 . '=' );
223 + }
224 + }
225 + }
226 +
227 + return implode( '&', $return );
228 + }
229 +
230 +
231 + /**
232 + * Returns all request methods and parameters
233 + *
234 + * @return string
235 + */
236 + private function capture_request() {
237 + $data = $this->capture_keys();
238 +
239 + // Get the method and URL.
240 + $method = $_SERVER['REQUEST_METHOD'];
241 + $rulesUri = $_SERVER['REQUEST_URI'];
242 +
243 + // Store the header values in different formats.
244 + $rulesHeadersKeys = array();
245 + $rulesHeadersValues = array();
246 + $rulesHeadersCombinations = array();
247 +
248 + // Retrieve the headers.
249 + $headers = $this->get_headers();
250 + $rulesHeadersAll = implode( ' ', $headers );
251 + foreach ( $headers as $name => $value ) {
252 + $rulesHeadersKeys[] = $name;
253 + $rulesHeadersValues[] = $value;
254 + $rulesHeadersCombinations[] = $name . ': ' . $value;
255 + }
256 +
257 + // Store the $_POST values in different formats.
258 + $rulesBodyKeys = array();
259 + $rulesBodyValues = array();
260 + $rulesBodyCombinations = array();
261 +
262 + // Retrieve the $_POST values.
263 + $rulesBodyAll = urldecode( http_build_query( $data['POST'] ) );
264 + foreach ( $data['POST'] as $key => $value ) {
265 + if ( is_array( $value ) ) {
266 + $value = @$this->multi_implode( $value, ' ' );
267 + }
268 + $rulesBodyKeys[] = $key;
269 + $rulesBodyValues[] = $value;
270 + $rulesBodyCombinations[] = $key . '=' . $value;
271 + }
272 +
273 + // Store the $_GET values in different formats.
274 + $rulesParamsKeys = array();
275 + $rulesParamsValues = array();
276 + $rulesParamsCombinations = array();
277 +
278 + // Retrieve the $_GET values.
279 + $rulesParamsAll = urldecode( http_build_query( $data['GET'] ) );
280 + foreach ( $data['GET'] as $key => $value ) {
281 + if ( is_array( $value ) ) {
282 + $value = @$this->multi_implode( $value, ' ' );
283 + }
284 + $rulesParamsKeys[] = $key;
285 + $rulesParamsValues[] = $value;
286 + $rulesParamsCombinations[] = $key . '=' . $value;
287 + }
288 +
289 + // Raw POST data.
290 + $rulesRawPost = @file_get_contents( 'php://input' );
291 +
292 + // Data about file uploads.
293 + $rulesFile = $this->get_file_upload_data();
294 +
295 + // Return each value as its own array.
296 + return compact(
297 + 'method',
298 + 'rulesFile',
299 + 'rulesRawPost',
300 + 'rulesUri',
301 + 'rulesHeadersAll',
302 + 'rulesHeadersKeys',
303 + 'rulesHeadersValues',
304 + 'rulesHeadersCombinations',
305 + 'rulesBodyAll',
306 + 'rulesBodyKeys',
307 + 'rulesBodyValues',
308 + 'rulesBodyCombinations',
309 + 'rulesParamsAll',
310 + 'rulesParamsKeys',
311 + 'rulesParamsValues',
312 + 'rulesParamsCombinations'
313 + );
314 + }
315 +
316 + /**
317 + * Capture the keys of the request.
318 + *
319 + * @return array
320 + */
321 + private function capture_keys() {
322 + // Data we want to go through.
323 + $data = array(
324 + 'POST' => $_POST,
325 + 'GET' => $_GET,
326 + );
327 +
328 + // Determine if there are any keys we should remove from the data set.
329 + if ( get_option( 'patchstack_whitelist_keys_rules', '' ) == '' ) {
330 + return $data;
331 + }
332 +
333 + // Must be valid JSON and decodes to at least 2 primary data arrays.
334 + $keys = json_decode( get_option( 'patchstack_whitelist_keys_rules' ), true );
335 + if ( ! $keys || ! is_array( $keys ) || $keys && count( $keys ) < 2 ) {
336 + return $data;
337 + }
338 +
339 + // Remove the keys where necessary, go through all data types (GET, POST).
340 + foreach ( $keys as $type => $entries ) {
341 +
342 + // Go through all whitelisted actions.
343 + foreach ( $entries as $entry ) {
344 + $t = explode( '.', $entry );
345 +
346 + // For non-multidimensional array checks.
347 + if ( count( $t ) == 1 ) {
348 + // If the value itself exists.
349 + if ( isset( $data[ $type ][ $t[0] ] ) ) {
350 + unset( $data[ $type ][ $t[0] ] );
351 + }
352 +
353 + // For pattern checking.
354 + if ( strpos( $t[0], '*' ) !== false ) {
355 + $star = explode( '*', $t[0] );
356 +
357 + // Loop through all $_POST, $_GET values.
358 + foreach ( $data as $method => $values ) {
359 + foreach ( $values as $key => $value ) {
360 + if ( ! is_array( $value ) && strpos( $key, $star[0] ) !== false ) {
361 + unset( $data[ $method ][ $key ] );
362 + }
363 + }
364 + }
365 + }
366 + continue;
367 + }
368 +
369 + // For multidimensional array checks.
370 + $end =& $data[ $type ];
371 + $skip = false;
372 + foreach ( $t as $var ) {
373 + if ( ! isset( $end[ $var ] ) ) {
374 + $skip = true;
375 + break;
376 + }
377 + $end =& $end[ $var ];
378 + }
379 +
380 + // Since we cannot unset it due to it being a reference variable,
381 + // we just set it to an empty string instead.
382 + if ( ! $skip ) {
383 + $end = '';
384 + }
385 + }
386 + }
387 +
388 + return $data;
389 + }
390 +
391 + /**
392 + * Implode array recursively.
393 + *
394 + * @param $array
395 + * @param $glue
396 + * @return bool|string
397 + */
398 + private function multi_implode( $array, $glue ) {
399 + $ret = '';
400 +
401 + foreach ( $array as $item ) {
402 + if ( is_array( $item ) ) {
403 + $ret .= $this->multi_implode( $item, $glue ) . $glue;
404 + } else {
405 + $ret .= $item . $glue;
406 + }
407 + }
408 +
409 + return substr( $ret, 0, 0 - strlen( $glue ) );
410 + }
411 +
412 + /**
413 + * Determine if the request matches the given firewall or whitelist rule.
414 + *
415 + * @param string $rule
416 + * @param string|array $request
417 + * @return bool
418 + */
419 + private function is_rule_match( $rule, $request ) {
420 + $is_matched = false;
421 + if ( is_array( $request ) ) {
422 + foreach ( $request as $key => $value ) {
423 + $is_matched = $this->is_rule_match( $rule, $value );
424 + if ( $is_matched ) {
425 + return $is_matched;
426 + }
427 + }
428 + } else {
429 + return preg_match( $rule, urldecode( $request ) );
430 + }
431 +
432 + return $is_matched;
433 + }
434 +
435 + /**
436 + * Count the number of rules.
437 + *
438 + * @param array $array
439 + * @return integer
440 + */
441 + private function count_rules( $array ) {
442 + $counter = 0;
443 + if ( is_object( $array ) ) {
444 + $array = (array) $array;
445 + }
446 +
447 + if ( $array['uri'] ) {
448 + $counter++;
449 + }
450 +
451 + foreach ( array( 'body', 'params', 'headers' ) as $type ) {
452 + foreach ( $array[ $type ] as $key => $value ) {
453 + if ( ! is_null( $value ) ) {
454 + $counter++;
455 + }
456 + }
457 + }
458 +
459 + return $counter;
460 + }
461 +
462 + /**
463 + * Runs the firewall rules processor.
464 + *
465 + * @return void
466 + */
467 + private function processor() {
468 + // Load the firewall rules.
469 + $rules = json_decode( get_option( 'patchstack_firewall_rules', '' ), true );
470 + if ( $rules == '' || is_null( $rules ) ) {
471 + return;
472 + }
473 +
474 + // Determine if the user is temporarily blocked from the site.
475 + if ( $this->is_auto_ip_blocked() > $this->get_option( 'patchstack_autoblock_attempts', 10 ) && ! $this->is_authenticated() ) {
476 + $this->display_error_page( 22 );
477 + }
478 +
479 + // Check for whitelist.
480 + $is_whitelisted = $this->is_whitelisted();
481 +
482 + // Obtain the IP address and request data.
483 + $client_ip = $this->get_ip();
484 + $requests = $this->capture_request();
485 +
486 + // Iterate through all root objects.
487 + foreach ( $rules as $firewall_rule ) {
488 + $blocked_count = 0;
489 + $firewall_rule['bypass_whitelist'] = isset( $firewall_rule['bypass_whitelist'] ) ? $firewall_rule['bypass_whitelist'] : false;
490 +
491 + // Do we need to skip the whitelist for a particular rule?
492 + if ( isset( $firewall_rule['bypass_whitelist'] ) && ! $firewall_rule['bypass_whitelist'] && $is_whitelisted ) {
493 + continue;
494 + }
495 +
496 + $rule_terms = json_decode( $firewall_rule['rule'] );
497 +
498 + // Determine if we should match the IP address.
499 + $ip = isset( $rule_terms->rules->ip_address ) ? $rule_terms->rules->ip_address : null;
500 + if ( ! is_null( $ip ) ) {
501 + $matched_ip = false;
502 + if ( strpos( $ip, '*' ) !== false ) {
503 + $matched_ip = $this->plugin->ban->check_wildcard_rule( $client_ip, $ip );
504 + } elseif ( strpos( $ip, '-' ) !== false ) {
505 + $matched_ip = $this->plugin->ban->check_range_rule( $client_ip, $ip );
506 + } elseif ( strpos( $ip, '/' ) !== false ) {
507 + $matched_ip = $this->plugin->ban->check_subnet_mask_rule( $client_ip, $ip );
508 + } elseif ( $client_ip == $ip ) {
509 + $matched_ip = true;
510 + }
511 +
512 + if ( ! $matched_ip ) {
513 + continue;
514 + }
515 + }
516 +
517 + // If matches on all request methods, only 1 rule match is required to block
518 + if ( $rule_terms->method === 'ALL' ) {
519 + $count_rules = 1;
520 + } else {
521 + $count_rules = json_decode( json_encode( $rule_terms->rules ), true );
522 + $count_rules = $this->count_rules( $count_rules );
523 + }
524 +
525 + // Loop through all request data that we captured.
526 + foreach ( $requests as $key => $request ) {
527 +
528 + // Treat the raw POST data string as the body contents of all values combined.
529 + if ( $key == 'rulesRawPost' ) {
530 + $key = 'rulesBodyAll';
531 + }
532 +
533 + // Determine if the requesting method matches.
534 + if ( $rule_terms->method == $requests['method'] || $rule_terms->method == 'ALL' || $rule_terms->method == 'GET' || ( $rule_terms->method == 'FILES' && $this->is_file_upload() ) ) {
535 + $test = strtolower( preg_replace( '/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key ) );
536 + $exp = explode( '->', $test );
537 +
538 + // Determine if a rule exists for this request.
539 + $rule = array_reduce(
540 + $exp,
541 + function ( $o, $p ) {
542 + if ( ! isset( $o->$p ) ) {
543 + return null;
544 + }
545 +
546 + return $o->$p;
547 + },
548 + $rule_terms
549 + );
550 +
551 + // Determine if the rule matches the request.
552 + if ( ! is_null( $rule ) && substr( $key, 0, 4 ) == 'rule' && $this->is_rule_match( $rule, $request ) ) {
553 + $blocked_count++;
554 + }
555 + }
556 + }
557 +
558 + // Determine if the user should be blocked.
559 + if ( $blocked_count >= $count_rules ) {
560 + if ( $rule_terms->type == 'BLOCK' ) {
561 + $this->block_user( $firewall_rule['id'], (bool) $firewall_rule['bypass_whitelist'] );
562 + } elseif ( $rule_terms->type == 'LOG' ) {
563 + $this->log_user( $firewall_rule['id'] );
564 + } elseif ( $rule_terms->type == 'REDIRECT' ) {
565 + $this->redirect_user( $firewall_rule['id'], $rule_terms->type_params );
566 + }
567 + }
568 + }
569 + }
570 +
571 + /**
572 + * Determine if the current request is a file upload.
573 + *
574 + * @return boolean
575 + */
576 + private function is_file_upload() {
577 + return isset( $_FILES ) && count( $_FILES ) > 0;
578 + }
579 +
580 + /**
581 + * Automatically block the user if there are many blocked requests in a short period of time.
582 + *
583 + * @return integer
584 + */
585 + public function is_auto_ip_blocked() {
586 + // Calculate block time.
587 + $minutes = (int) $this->get_option( 'patchstack_autoblock_minutes', 30 );
588 + $timeout = (int) $this->get_option( 'patchstack_autoblock_blocktime', 60 );
589 + if ( empty( $minutes ) || empty( $timeout ) ) {
590 + $time = 30 + 60;
591 + } else {
592 + $time = $minutes + $timeout;
593 + }
594 +
595 + // Determine if the user should be blocked.
596 + global $wpdb;
597 + $results = $wpdb->get_results(
598 + $wpdb->prepare( 'SELECT COUNT(*) as numIps FROM ' . $wpdb->prefix . "patchstack_firewall_log WHERE block_type = 'BLOCK' AND apply_ban = 1 AND ip = '%s' AND log_date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE)", array( $this->get_ip(), $time ) ),
599 + OBJECT
600 + );
601 +
602 + if ( ! isset( $results, $results[0], $results[0]->numIps ) ) {
603 + return 0;
604 + }
605 + return $results[0]->numIps;
606 + }
607 +
608 + /**
609 + * Block the user, and log, do whatever is necessary.
610 + *
611 + * @param string $rule
612 + * @param bool $bypass
613 + * @return void
614 + */
615 + private function block_user( $rule, $bypass = false ) {
616 + if ( ! $this->is_authenticated( $bypass ) ) {
617 + $this->display_error_page( '55' . intval( $rule ) );
618 + }
619 + }
620 +
621 + /**
622 + * Log the user action.
623 + *
624 + * @param string $rule
625 + * @return void
626 + */
627 + private function log_user( $rule ) {
628 + $this->log_hacker( $rule, '', 'LOG' );
629 + }
630 +
631 + /**
632 + * Log the user action and redirect.
633 + *
634 + * @param integer $rule_id
635 + * @param string $redirect
636 + * @return void
637 + */
638 + private function redirect_user( $rule_id, $redirect ) {
639 + $this->log_hacker( $rule_id, '', 'REDIRECT' );
640 +
641 + // Don't redirect an invalid URL.
642 + if ( ! $redirect || stripos( $redirect, 'http' ) === false ) {
643 + return;
644 + }
645 +
646 + ob_start();
647 + header( 'Location: ' . $redirect );
648 + ob_end_flush();
649 + exit;
650 + }
651 +
652 + /**
653 + * Determine if the user is authenticated and in the list of whitelisted roles.
654 + *
655 + * @param bool $bypass
656 + * @return bool
657 + */
658 + public function is_authenticated( $bypass = false ) {
659 + if ( $bypass || ! is_user_logged_in() ) {
660 + return false;
661 + }
662 +
663 + // Get the whitelisted roles.
664 + $roles = $this->get_option( 'patchstack_basic_firewall_roles', array( 'administrator', 'editor', 'author' ) );
665 + if ( ! is_array ( $roles ) ) {
666 + return false;
667 + }
668 +
669 + // Special scenario for super admins on a multisite environment.
670 + if ( in_array( 'administrator', $roles ) && is_multisite() && is_super_admin() ) {
671 + return true;
672 + }
673 +
674 + // User is logged in, determine the role.
675 + $user = wp_get_current_user();
676 + if ( ! isset( $user->roles ) || count( (array) $user->roles ) == 0 ) {
677 + return false;
678 + }
679 +
680 + // Is the user in the whitelist roles list?
681 + $role_count = array_intersect( $user->roles, $roles );
682 + return count( $role_count ) != 0;
683 + }
684 +
685 + /**
686 + * Log the blocked request.
687 + *
688 + * @param integer $fid firewall
689 + * @param array $query_vars
690 + * @param string $block_type
691 + * @param array $block_params
692 + * @return void
693 + */
694 + private function log_hacker( $fid = 1, $post_data = '', $block_type = 'BLOCK' ) {
695 + global $wpdb;
696 + if ( ! $wpdb || $fid == 22 ) {
697 + return;
698 + }
699 +
700 + // Insert into the logs.
701 + $wpdb->insert(
702 + $wpdb->prefix . 'patchstack_firewall_log',
703 + array(
704 + 'ip' => $this->get_ip(),
705 + 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '',
706 + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
707 + 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '',
708 + 'fid' => $fid,
709 + 'flag' => '',
710 + 'post_data' => $post_data != '' ? json_encode( $post_data ) : $this->get_post_data(),
711 + 'block_type' => $block_type,
712 + )
713 + );
714 + }
715 +
716 + /**
717 + * Get POST data.
718 + *
719 + * @return string|NULL
720 + */
721 + private function get_post_data() {
722 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || $_SERVER['REQUEST_METHOD'] != 'POST' ) {
723 + return null;
724 + }
725 +
726 + return json_encode( $_POST );
727 + }
728 +
729 + /**
730 + * Display error page.
731 + *
732 + * @param integer $fid
733 + * @return void
734 + */
735 + public function display_error_page( $fid = 1 ) {
736 + if ( $fid != 22 ) {
737 + $this->log_hacker( $fid );
738 + }
739 +
740 + header( 'Cache-Control: no-store' );
741 + header( 'Pragma: no-cache' );
742 + http_response_code( 403 );
743 + require_once dirname( __FILE__ ) . '/views/access-denied.php';
744 + exit;
745 + }
746 +}