PluginProbe
Patchstack – WordPress & Plugins Security / trunk
Patchstack – WordPress & Plugins Security vtrunk
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
patchstack / includes / firewall.php

firewall.php in Patchstack – WordPress & Plugins Security trunk, at includes/firewall.php

218 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
218