PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.4
Patchstack – WordPress & Plugins Security v2.3.4
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.4, at includes/hardening.php

323 lines 10.1 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 // Apply comment captcha?
39 if ( $this->get_option( 'patchstack_captcha_on_comments', 0 ) && ! is_user_logged_in() ) {
40 add_action( 'comment_form_after_fields', [ $this, 'captcha_display' ] );
41 add_filter( 'preprocess_comment', [ $this, 'verify_recaptcha' ] );
42 }
43
44 // Disable the application passwords feature?
45 if ( $this->get_option( 'patchstack_application_passwords_disabled', false ) == true ) {
46 add_filter( 'wp_is_application_passwords_available', '__return_false' );
47 }
48
49 // Block unauthorized XML-RPC requests?
50 if ( $this->get_option( 'patchstack_xmlrpc_is_disabled', false ) == true ) {
51 add_filter( 'xmlrpc_enabled', '__return_false' );
52 }
53
54 // Block unauthorized wp-json requests?
55 if ( $this->get_option( 'patchstack_json_is_disabled', false ) ) {
56 add_filter( 'rest_authentication_errors', [ $this, 'disable_wpjson' ] );
57 }
58
59 // Prevent user enumeration?
60 if ( $this->get_option( 'patchstack_userenum' ) ) {
61 add_action( 'init', [ $this, 'stop_user_enum' ], 1 );
62 }
63
64 // Attempt to hide the WordPress version?
65 if ( $this->get_option( 'patchstack_hidewpversion' ) ) {
66 remove_action( 'wp_head', 'wp_generator' );
67 add_filter( 'the_generator', [ $this, 'remove_generator' ] );
68 }
69
70 // Auto update software?
71 $update = get_site_option( 'patchstack_auto_update', [] );
72 if ( is_array( $update ) ) {
73 foreach ( $update as $type ) {
74 if ( $type != 'vulnerable' ) {
75 add_filter( 'auto_update_' . $type, '__return_true' );
76 }
77 }
78 }
79 }
80
81 /**
82 * Perform updates if the software upload call returns vulnerabilities.
83 * This is only executed when auto updates are enabled for vulnerable plugins.
84 *
85 * @param array $plugins
86 * @return void
87 */
88 public function update_vulnerable_plugins() {
89 // Is the auto update setting for vulnerable plugins enabled?
90 $update = get_site_option( 'patchstack_auto_update', [] );
91 if ( ! is_array( $update ) || ! in_array( 'vulnerable', $update ) ) {
92 return;
93 }
94
95 // Do we even have any vulnerable plugins to auto update?
96 $plugins = get_site_option( 'patchstack_vulnerable_plugins', [] );
97 if ( ! is_array( $plugins ) || count( $plugins ) == 0 ) {
98 return;
99 }
100
101 // Might not be necessary, but should prevent any hanging issues.
102 @set_time_limit( 180 );
103
104 // Require some files we need to execute the upgrade.
105 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
106 if ( file_exists( ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ) ) {
107 @include_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
108 }
109
110 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
111 @include_once ABSPATH . 'wp-admin/includes/misc.php';
112 @include_once ABSPATH . 'wp-admin/includes/file.php';
113 @wp_update_plugins();
114 $all_plugins = get_plugins();
115
116 // New array with all available plugins and the ones we want to upgrade.
117 $upgrade = [];
118 foreach ( $all_plugins as $path => $data ) {
119 if ( in_array( $path, $plugins ) ) {
120 array_push( $upgrade, $path );
121 }
122 }
123
124 // Upgrade the plugins.
125 $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
126 $upgrader->bulk_upgrade( $upgrade );
127
128 // Reset the option that holds the vulnerable plugins.
129 update_site_option( 'patchstack_vulnerable_plugins', [] );
130
131 // Resend the sofware data to the API.
132 do_action( 'patchstack_send_software_data' );
133 }
134
135 /**
136 * Prevent unauthorized users from accessing wp-json.
137 *
138 * @return void|WP_Error
139 */
140 public function disable_wpjson() {
141 // Some default exceptions.
142 $path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
143 $whitelists = [ '/wp-json/contact-form-7/' ];
144 foreach ( $whitelists as $whitelist ) {
145 if ( stripos( $path, $whitelist ) !== false ) {
146 return;
147 }
148 }
149
150 // Block unauthorized users.
151 if ( ! is_user_logged_in() ) {
152 $msg = apply_filters( 'disable_wp_rest_api_error', esc_attr__( 'The WP REST API cannot be accessed by unauthorized users.', 'disable-wp-rest-api' ) );
153 return new WP_Error( 'rest_authorization_required', $msg, [ 'status' => rest_authorization_required_code() ] );
154 }
155 }
156
157 /**
158 * Set security headers if the option is enabled.
159 *
160 * @param array $headers
161 * @return void|array
162 */
163 public function set_security_headers( $headers ) {
164 if ( get_option( 'patchstack_add_security_headers' ) ) {
165 $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
166 $headers['X-Frame-Options'] = 'SAMEORIGIN';
167 $headers['X-XSS-Protection'] = '1; mode=block';
168 $headers['X-Content-Type-Options'] = 'nosniff';
169 $headers['X-Powered-By'] = null;
170 $headers['Server'] = null;
171 $headers['Strict-Transport-Security'] = 'max-age=31536000';
172 }
173
174 return $headers;
175 }
176
177 /**
178 * Determine if the reCAPTCHA is valid upon comment submission.
179 *
180 * @param array $comment_data
181 * @return void|array
182 */
183 public function verify_recaptcha( $comment_data ) {
184 $result = $this->captcha_check();
185 if ( ! $result['response'] && ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) ) {
186 wp_clear_auth_cookie();
187 wp_die( 'reCaptcha was not solved or response was empty', 'Error' );
188 }
189
190 return $comment_data;
191 }
192
193 /**
194 * Add the captcha to the comments form.
195 *
196 * @return void
197 */
198 public function captcha_display() {
199 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
200 case 'v2':
201 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
202 require dirname( __FILE__ ) . '/views/captcha_v2.php';
203 break;
204 case 'invisible':
205 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
206 require dirname( __FILE__ ) . '/views/captcha_invisible.php';
207 break;
208 case 'v3':
209 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
210 require dirname( __FILE__ ) . '/views/captcha_v3.php';
211 break;
212 case 'turnstile':
213 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
214 require dirname( __FILE__ ) . '/views/captcha_turnstile.php';
215 break;
216 }
217 }
218
219 /**
220 * Check if the submitted reCAPTCHA is valid.
221 *
222 * @return array
223 */
224 public function captcha_check() {
225 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
226 case 'v2':
227 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
228 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
229 break;
230 case 'invisible':
231 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
232 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
233 break;
234 case 'v3':
235 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
236 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
237 break;
238 case 'turnstile':
239 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_turnstile' ) );
240 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
241 break;
242 }
243
244 if ( ! $secret_key || ! $site_key ) {
245 return [
246 'response' => false,
247 'reason' => 'ERROR_NO_KEYS',
248 ];
249 }
250
251 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
252 return [
253 'response' => false,
254 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
255 ];
256 }
257
258 $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
259 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
260 return [
261 'response' => true,
262 'reason' => '',
263 ];
264 }
265
266 return [
267 'response' => false,
268 'reason' => 'VERIFICATION_FAILED',
269 ];
270 }
271
272 /**
273 * Query Google for reAPTCHA validation and response.
274 *
275 * @param string $privatekey
276 * @param string $type
277 * @return array
278 */
279 public function get_captcha_response( $privatekey, $type ) {
280 $args = [
281 'body' => [
282 'secret' => $privatekey,
283 'response' => $_POST['g-recaptcha-response'],
284 ],
285 'sslverify' => false,
286 ];
287
288 if ($type != 'turnstile') {
289 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
290 } else {
291 $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', $args );
292 }
293
294 return json_decode( wp_remote_retrieve_body( $resp ), true );
295 }
296
297 /**
298 * Disable user enumeration with ?author= and the REST endpoint.
299 *
300 * @return void
301 */
302 public function stop_user_enum() {
303 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
304 die( wp_safe_redirect( get_site_url() ) );
305 }
306
307 if ( stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
308 if ( ! is_user_logged_in() ) {
309 die( wp_safe_redirect( get_site_url() ) );
310 }
311 }
312 }
313
314 /**
315 * Hide the WordPress generator version in response.
316 *
317 * @return string
318 */
319 public function remove_generator() {
320 return '';
321 }
322 }
323