PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.0
Patchstack – WordPress & Plugins Security v2.3.0
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 / hardening.php

hardening.php in Patchstack – WordPress & Plugins Security 2.3.0, at includes/hardening.php

369 lines 11.7 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 is used to provide several hardening options.
10 */
11 class P_Hardening extends P_Core {
12
13 /**
14 * Add the actions required for the hardening of the site.
15 *
16 * @param Patchstack $core
17 * @return void
18 */
19 public function __construct( $core ) {
20 parent::__construct( $core );
21
22 // Auto update plugins.
23 add_action( 'patchstack_update_plugins', [ $this, 'update_vulnerable_plugins' ] );
24
25 // The hardening features can only be used on an activated license.
26 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
27 return;
28 }
29
30 // Disallowed modification of the theme files?
31 if ( ! defined( 'DISALLOW_FILE_EDIT' ) && $this->get_option( 'patchstack_pluginedit', true ) ) {
32 define( 'DISALLOW_FILE_EDIT', 1 );
33 }
34
35 // Set security headers
36 add_filter( 'wp_headers', [ $this, 'set_security_headers' ], 10, 1 );
37
38 // When country blocking is set.
39 if ( $this->get_option( 'patchstack_geo_block_enabled', false ) && ! empty( $this->get_option( 'patchstack_geo_block_countries', [] ) ) ) {
40 add_action( 'init', array( $this, 'geo_block_check' ), ~PHP_INT_MAX );
41 }
42
43 // Apply comment captcha?
44 if ( $this->get_option( 'patchstack_captcha_on_comments', 0 ) && ! is_user_logged_in() ) {
45 add_action( 'comment_form_after_fields', [ $this, 'captcha_display' ] );
46 add_filter( 'preprocess_comment', [ $this, 'verify_recaptcha' ] );
47 }
48
49 // Disable the application passwords feature?
50 if ( $this->get_option( 'patchstack_application_passwords_disabled', false ) == true ) {
51 add_filter( 'wp_is_application_passwords_available', '__return_false' );
52 }
53
54 // Block unauthorized XML-RPC requests?
55 if ( $this->get_option( 'patchstack_xmlrpc_is_disabled', false ) == true ) {
56 add_filter( 'xmlrpc_enabled', '__return_false' );
57 }
58
59 // Block unauthorized wp-json requests?
60 if ( $this->get_option( 'patchstack_json_is_disabled', false ) ) {
61 add_filter( 'rest_authentication_errors', [ $this, 'disable_wpjson' ] );
62 }
63
64 // Prevent user enumeration?
65 if ( $this->get_option( 'patchstack_userenum' ) ) {
66 add_action( 'init', [ $this, 'stop_user_enum' ], 1 );
67 }
68
69 // Attempt to hide the WordPress version?
70 if ( $this->get_option( 'patchstack_hidewpversion' ) ) {
71 remove_action( 'wp_head', 'wp_generator' );
72 add_filter( 'the_generator', [ $this, 'remove_generator' ] );
73 }
74
75 // Auto update software?
76 $update = get_site_option( 'patchstack_auto_update', [] );
77 if ( is_array( $update ) ) {
78 foreach ( $update as $type ) {
79 if ( $type != 'vulnerable' ) {
80 add_filter( 'auto_update_' . $type, '__return_true' );
81 }
82 }
83 }
84 }
85
86 /**
87 * Perform updates if the software upload call returns vulnerabilities.
88 * This is only executed when auto updates are enabled for vulnerable plugins.
89 *
90 * @param array $plugins
91 * @return void
92 */
93 public function update_vulnerable_plugins() {
94 // Is the auto update setting for vulnerable plugins enabled?
95 $update = get_site_option( 'patchstack_auto_update', [] );
96 if ( ! is_array( $update ) || ! in_array( 'vulnerable', $update ) ) {
97 return;
98 }
99
100 // Do we even have any vulnerable plugins to auto update?
101 $plugins = get_site_option( 'patchstack_vulnerable_plugins', [] );
102 if ( ! is_array( $plugins ) || count( $plugins ) == 0 ) {
103 return;
104 }
105
106 // Might not be necessary, but should prevent any hanging issues.
107 @set_time_limit( 180 );
108
109 // Require some files we need to execute the upgrade.
110 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
111 if ( file_exists( ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ) ) {
112 @include_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
113 }
114
115 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
116 @include_once ABSPATH . 'wp-admin/includes/misc.php';
117 @include_once ABSPATH . 'wp-admin/includes/file.php';
118 @wp_update_plugins();
119 $all_plugins = get_plugins();
120
121 // New array with all available plugins and the ones we want to upgrade.
122 $upgrade = [];
123 foreach ( $all_plugins as $path => $data ) {
124 if ( in_array( $path, $plugins ) ) {
125 array_push( $upgrade, $path );
126 }
127 }
128
129 // Upgrade the plugins.
130 $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
131 $upgrader->bulk_upgrade( $upgrade );
132
133 // Reset the option that holds the vulnerable plugins.
134 update_site_option( 'patchstack_vulnerable_plugins', [] );
135
136 // Resend the sofware data to the API.
137 do_action( 'patchstack_send_software_data' );
138 }
139
140 /**
141 * Determine the country of the user and if we should block the user.
142 *
143 * @return void
144 */
145 public function geo_block_check() {
146 $countries = $this->get_option( 'patchstack_geo_block_countries', [] );
147 $ip = $this->get_ip();
148
149 // Don't block Patchstack.
150 if ( ( isset( $_POST['patchstack_secret'] ) && $this->plugin->listener->verifyToken( $_POST['patchstack_secret'] ) ) || isset( $_POST['patchstack_ott_action'] )) {
151
152 // OTT action.
153 if ( isset( $_POST['patchstack_ott_action'] ) ) {
154 $ott = get_option( 'patchstack_ott_action', '' );
155 if ( ! empty( $ott ) && hash_equals( $ott, $_POST['patchstack_ott_action'] ) ) {
156 return;
157 }
158 } else {
159 return;
160 }
161 }
162
163 // Load the required libraries.
164 try {
165 require_once __DIR__ . '/../lib/geoip2-php/autoload.php';
166 $reader = new GeoIp2\Database\Reader( __DIR__ . '/../lib/GeoLite2-Country.mmdb' );
167 $record = $reader->country( $ip );
168
169 // Determine if we want to do an inverse check or not.
170 $match = in_array( $record->country->isoCode, $countries );
171 $match = $this->get_option( 'patchstack_geo_block_inverse', false ) ? ! $match : $match;
172
173 // Check if there's a match.
174 if ( $match ) {
175 $this->plugin->firewall_base->display_error_page( 23 );
176 }
177 } catch ( \Exception $e ) {
178 }
179 }
180
181 /**
182 * Prevent unauthorized users from accessing wp-json.
183 *
184 * @return void|WP_Error
185 */
186 public function disable_wpjson() {
187 // Some default exceptions.
188 $path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
189 $whitelists = [ '/wp-json/contact-form-7/' ];
190 foreach ( $whitelists as $whitelist ) {
191 if ( stripos( $path, $whitelist ) !== false ) {
192 return;
193 }
194 }
195
196 // Block unauthorized users.
197 if ( ! is_user_logged_in() ) {
198 $msg = apply_filters( 'disable_wp_rest_api_error', esc_attr__( 'The WP REST API cannot be accessed by unauthorized users.', 'disable-wp-rest-api' ) );
199 return new WP_Error( 'rest_authorization_required', $msg, [ 'status' => rest_authorization_required_code() ] );
200 }
201 }
202
203 /**
204 * Set security headers if the option is enabled.
205 *
206 * @param array $headers
207 * @return void|array
208 */
209 public function set_security_headers( $headers ) {
210 if ( get_option( 'patchstack_add_security_headers' ) ) {
211 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
212 $headers['X-Frame-Options'] = 'SAMEORIGIN';
213 $headers['X-XSS-Protection'] = '1; mode=block';
214 $headers['X-Content-Type-Options'] = 'nosniff';
215 $headers['X-Powered-By'] = null;
216 $headers['Server'] = null;
217 $headers['Strict-Transport-Security'] = 'max-age=31536000';
218 }
219
220 return $headers;
221 }
222
223 /**
224 * Determine if the reCAPTCHA is valid upon comment submission.
225 *
226 * @param array $comment_data
227 * @return void|array
228 */
229 public function verify_recaptcha( $comment_data ) {
230 $result = $this->captcha_check();
231 if ( ! $result['response'] && ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) ) {
232 wp_clear_auth_cookie();
233 wp_die( 'reCaptcha was not solved or response was empty', 'Error' );
234 }
235
236 return $comment_data;
237 }
238
239 /**
240 * Add the captcha to the comments form.
241 *
242 * @return void
243 */
244 public function captcha_display() {
245 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
246 case 'v2':
247 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
248 require dirname( __FILE__ ) . '/views/captcha_v2.php';
249 break;
250 case 'invisible':
251 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
252 require dirname( __FILE__ ) . '/views/captcha_invisible.php';
253 break;
254 case 'v3':
255 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
256 require dirname( __FILE__ ) . '/views/captcha_v3.php';
257 break;
258 case 'turnstile':
259 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
260 require dirname( __FILE__ ) . '/views/captcha_turnstile.php';
261 break;
262 }
263 }
264
265 /**
266 * Check if the submitted reCAPTCHA is valid.
267 *
268 * @return array
269 */
270 public function captcha_check() {
271 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
272 case 'v2':
273 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
274 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
275 break;
276 case 'invisible':
277 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
278 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
279 break;
280 case 'v3':
281 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
282 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
283 break;
284 case 'turnstile':
285 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_turnstile' ) );
286 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
287 break;
288 }
289
290 if ( ! $secret_key || ! $site_key ) {
291 return [
292 'response' => false,
293 'reason' => 'ERROR_NO_KEYS',
294 ];
295 }
296
297 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
298 return [
299 'response' => false,
300 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
301 ];
302 }
303
304 $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
305 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
306 return [
307 'response' => true,
308 'reason' => '',
309 ];
310 }
311
312 return [
313 'response' => false,
314 'reason' => 'VERIFICATION_FAILED',
315 ];
316 }
317
318 /**
319 * Query Google for reAPTCHA validation and response.
320 *
321 * @param string $privatekey
322 * @param string $type
323 * @return array
324 */
325 public function get_captcha_response( $privatekey, $type ) {
326 $args = [
327 'body' => [
328 'secret' => $privatekey,
329 'response' => $_POST['g-recaptcha-response'],
330 ],
331 'sslverify' => false,
332 ];
333
334 if ($type != 'turnstile') {
335 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
336 } else {
337 $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', $args );
338 }
339
340 return json_decode( wp_remote_retrieve_body( $resp ), true );
341 }
342
343 /**
344 * Disable user enumeration with ?author= and the REST endpoint.
345 *
346 * @return void
347 */
348 public function stop_user_enum() {
349 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
350 die( wp_safe_redirect( get_site_url() ) );
351 }
352
353 if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
354 if ( ! is_user_logged_in() ) {
355 die( wp_safe_redirect( get_site_url() ) );
356 }
357 }
358 }
359
360 /**
361 * Hide the WordPress generator version in response.
362 *
363 * @return string
364 */
365 public function remove_generator() {
366 return '';
367 }
368 }
369