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 / hardening.php

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

324 lines 10.2 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 = isset( $_SERVER['REQUEST_URI'] ) ? (string) 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 $secret_key = '';
226 $site_key = '';
227 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
228 case 'v2':
229 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key' ) );
230 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key' ) );
231 break;
232 case 'invisible':
233 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3' ) );
234 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3' ) );
235 break;
236 case 'v3':
237 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_v3_new' ) );
238 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_v3_new' ) );
239 break;
240 case 'turnstile':
241 $secret_key = trim( $this->get_option( 'patchstack_captcha_private_key_turnstile' ) );
242 $site_key = trim( $this->get_option( 'patchstack_captcha_public_key_turnstile' ) );
243 break;
244 }
245
246 if ( ! $secret_key || ! $site_key ) {
247 return [
248 'response' => false,
249 'reason' => 'ERROR_NO_KEYS',
250 ];
251 }
252
253 if ( ! isset( $_POST['g-recaptcha-response'] ) || empty( $_POST['g-recaptcha-response'] ) ) {
254 return [
255 'response' => false,
256 'reason' => 'RECAPTCHA_EMPTY_RESPONSE',
257 ];
258 }
259
260 $response = $this->get_captcha_response( $secret_key, $this->get_option( 'patchstack_captcha_type' ) );
261 if ( isset( $response['success'] ) && ! empty( $response['success'] ) ) {
262 return [
263 'response' => true,
264 'reason' => '',
265 ];
266 }
267
268 return [
269 'response' => false,
270 'reason' => 'VERIFICATION_FAILED',
271 ];
272 }
273
274 /**
275 * Query Google for reAPTCHA validation and response.
276 *
277 * @param string $privatekey
278 * @param string $type
279 * @return array
280 */
281 public function get_captcha_response( $privatekey, $type ) {
282 $args = [
283 'body' => [
284 'secret' => $privatekey,
285 'response' => $_POST['g-recaptcha-response'],
286 ],
287 ];
288
289 if ($type != 'turnstile') {
290 $resp = wp_remote_post( 'https://www.google.com/recaptcha/api/siteverify', $args );
291 } else {
292 $resp = wp_remote_post( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', $args );
293 }
294
295 return json_decode( wp_remote_retrieve_body( $resp ), true );
296 }
297
298 /**
299 * Disable user enumeration with ?author= and the REST endpoint.
300 *
301 * @return void
302 */
303 public function stop_user_enum() {
304 if ( isset( $_GET['author'] ) && ! is_user_logged_in() && ! is_admin() ) {
305 die( wp_safe_redirect( get_site_url() ) );
306 }
307
308 if ( ( isset( $_SERVER['REQUEST_URI'] ) && stripos( $_SERVER['REQUEST_URI'], 'v2/users' ) !== false ) || ( isset( $_REQUEST['rest_route'] ) && stripos( $_REQUEST['rest_route'], 'v2/users' ) !== false ) ) {
309 if ( ! is_user_logged_in() ) {
310 die( wp_safe_redirect( get_site_url() ) );
311 }
312 }
313 }
314
315 /**
316 * Hide the WordPress generator version in response.
317 *
318 * @return string
319 */
320 public function remove_generator() {
321 return '';
322 }
323 }
324