PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.3
Defender Security – Malware Scanner, Login Security & Firewall v6.2.3
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / security-tweaks / class-prevent-enum-users.php

class-prevent-enum-users.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.3, at src/component/security-tweaks/class-prevent-enum-users.php

379 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Prevents user enumeration on a WordPress site to enhance security by blocking various endpoints and methods that can
4 * be used to discover user information.
5 *
6 * @package WP_Defender\Component\Security_Tweaks
7 */
8
9 namespace WP_Defender\Component\Security_Tweaks;
10
11 use WP_Error;
12 use WP_REST_Response;
13 use WP_Sitemaps_Provider;
14 use WP_Defender\Traits\Security_Tweaks_Option;
15
16 /**
17 * Prevent user enumeration.
18 */
19 class Prevent_Enum_Users extends Abstract_Security_Tweaks implements Security_Key_Const_Interface {
20
21 use Security_Tweaks_Option;
22
23 /**
24 * The slug identifier for this tweak.
25 *
26 * @var string
27 */
28 public string $slug = 'prevent-enum-users';
29
30 /**
31 * Status to check if the issue has been resolved.
32 *
33 * @var bool
34 */
35 public $resolved = false;
36
37 /**
38 * Enabled user enumerations list.
39 *
40 * @var array
41 */
42 private $enabled_user_enums = array();
43
44 /**
45 * Check whether the issue has been resolved or not.
46 *
47 * @return bool
48 */
49 public function check(): bool {
50 return $this->resolved;
51 }
52
53 /**
54 * Here is the code for processing.
55 *
56 * @return bool
57 */
58 public function process(): bool {
59 return true;
60 }
61
62 /**
63 * This is for un-do stuff that has been done in @process.
64 *
65 * @return bool
66 */
67 public function revert(): bool {
68 return true;
69 }
70
71 /**
72 * Shield up.
73 *
74 * @return void
75 */
76 public function shield_up() {
77 $this->resolved = true;
78
79 if ( is_admin() || defender_is_wp_cli() ) {
80 return;
81 }
82
83 add_filter( 'oembed_response_data', array( $this, 'oembed_author_filter' ), 99 );
84 add_filter( 'wp_sitemaps_users_pre_url_list', array( $this, 'block_user_url_sitemap' ), 99, 0 );
85 add_filter( 'wp_sitemaps_add_provider', array( $this, 'block_user_provider_sitemap' ), 99, 2 );
86 add_filter( 'rest_authentication_errors', array( $this, 'block_rest_api_user_endpoint' ) );
87 add_filter( 'rest_endpoints', array( $this, 'block_rest_api_user_endpoints' ), 99 );
88 add_action( 'rest_api_init', array( $this, 'register_dynamic_post_type_filters' ) );
89
90 $query = defender_get_data_from_request( 'QUERY_STRING', 's' );
91 if ( ! is_string( $query ) || '' === $query ) {
92 return;
93 }
94
95 $this->maybe_block( $query );
96
97 add_filter( 'redirect_canonical', array( $this, 'maybe_block' ) );
98 }
99
100 /**
101 * Maybe block the request if it's trying to access the author page with query param.
102 *
103 * @param string $request Request of current page.
104 *
105 * @return string
106 */
107 public function maybe_block( string $request ) {
108 if ( ! current_user_can( 'edit_others_posts' ) ) {
109 $params = array();
110 wp_parse_str( strtolower( $request ), $params );
111
112 if (
113 isset( $params['author'] )
114 && is_scalar( $params['author'] )
115 && '' !== preg_replace( '/[^0-9,-]/', '', $params['author'] )
116 ) {
117 wp_die(
118 esc_html__( 'Sorry, you are not allowed to access this page', 'defender-security' ),
119 esc_html__( 'Forbidden', 'defender-security' ),
120 403
121 );
122 }
123 }
124
125 return $request;
126 }
127
128 /**
129 * Retrieve the tweak's label.
130 *
131 * @return string
132 */
133 public function get_label(): string {
134 return esc_html__( 'Prevent user enumeration', 'defender-security' );
135 }
136
137 /**
138 * Get the error reason.
139 *
140 * @return string
141 */
142 public function get_error_reason(): string {
143 return wp_kses(
144 __( '<strong>User enumeration is currently allowed</strong> and may expose your site.', 'defender-security' ),
145 array(
146 'strong' => array(),
147 )
148 );
149 }
150
151 /**
152 * Return a summary data of this tweak.
153 *
154 * @return array
155 */
156 public function to_array(): array {
157 $enabled = $this->get_enabled_user_enums();
158 $stop_rest = in_array( 'enum-rest', $enabled, true );
159 $hide_user = in_array( 'enum-oembed', $enabled, true );
160 $disable_maps = in_array( 'enum-sitemap', $enabled, true );
161 $all_enabled = $stop_rest && $hide_user && $disable_maps;
162
163 $success_reason = $all_enabled
164 ? wp_strip_all_tags( __( 'All recommendation settings are turned on.', 'defender-security' ) )
165 : wp_strip_all_tags( __( 'Custom recommendation settings are turned on.', 'defender-security' ) );
166
167 return array(
168 'slug' => $this->slug,
169 'title' => $this->get_label(),
170 'errorReason' => $this->get_error_reason(),
171 'successReason' => $success_reason,
172 'misc' => array(
173 'show_revert_button' => true,
174 'show_action_button' => true,
175 'stop_rest_api' => $stop_rest,
176 'hide_user_id' => $hide_user,
177 'disable_author_sitemap' => $disable_maps,
178 ),
179 'bulk_description' => wp_strip_all_tags( __( 'Author redirects can make usernames easier for bots to find. Prevent user enumeration to protect your login area and reduce brute force risk.', 'defender-security' ) ),
180 'bulk_title' => wp_strip_all_tags( __( 'Prevent user enumeration', 'defender-security' ) ),
181 );
182 }
183
184 /**
185 * Getter method of enabled_user_enums.
186 *
187 * @return array Return array of enabled enumeration options.
188 */
189 public function get_enabled_user_enums(): array {
190 $enabled_user_enums = $this->get_option( 'enabled_user_enums' );
191
192 $this->enabled_user_enums = is_null( $enabled_user_enums )
193 ? array()
194 : (array) $enabled_user_enums;
195
196 return $this->enabled_user_enums;
197 }
198
199 /**
200 * Setter method of enabled_user_enums.
201 *
202 * @param array $value Array of enabled user enumeration options.
203 *
204 * @return bool Return true if value updated, otherwise false.
205 */
206 public function set_enabled_user_enums( $value ): bool {
207 $this->enabled_user_enums = ! is_array( $value ) ? (array) $value : $value;
208
209 return $this->update_option(
210 'enabled_user_enums',
211 $this->enabled_user_enums
212 );
213 }
214
215 /**
216 * Filters the author detail from oEmbed response data.
217 *
218 * @param array $data The response data.
219 *
220 * @return array $data The response data.
221 */
222 public function oembed_author_filter( $data ) {
223 if (
224 is_array( $data ) &&
225 in_array(
226 'enum-oembed',
227 $this->get_enabled_user_enums(),
228 true
229 )
230 ) {
231 unset( $data['author_name'], $data['author_url'] );
232 }
233
234 return $data;
235 }
236
237 /**
238 * Blocks user url generation in sitemap.
239 * Consider the URL: http://example.com/wp-sitemap-users-1.xml will not return user URL XML.
240 *
241 * @return bool|null If option sitemap enumeration enabled then return false else null/void will return.
242 */
243 public function block_user_url_sitemap(): ?bool {
244 if (
245 in_array(
246 'enum-sitemap',
247 $this->get_enabled_user_enums(),
248 true
249 )
250 ) {
251 return false;
252 }
253
254 return null;
255 }
256
257 /**
258 * Blocks the user provider sitemap before it is added from wp-sitemap.xml.
259 *
260 * @param WP_Sitemaps_Provider $provider Instance of a WP_Sitemaps_Provider.
261 * @param string $name Name of the sitemap provider.
262 *
263 * @return bool|WP_Sitemaps_Provider If option sitemap enumeration enabled then return false else
264 * WP_Sitemaps_Provider instance.
265 */
266 public function block_user_provider_sitemap( $provider, string $name ) {
267 if (
268 'users' === $name &&
269 in_array(
270 'enum-sitemap',
271 $this->get_enabled_user_enums(),
272 true
273 )
274 ) {
275 return false;
276 }
277
278 return $provider;
279 }
280
281 /**
282 * Block user REST API endpoint for not logged-in access.
283 *
284 * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
285 * method wasn't used, true if authentication succeeded.
286 *
287 * @return WP_Error|null|true $errors WP_Error if authentication error, null if authentication
288 * method wasn't used, true if authentication succeeded.
289 */
290 public function block_rest_api_user_endpoint( $errors ) {
291 $request_uri = (string) filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ?? '';
292 $rest_route = (string) filter_input( INPUT_GET, 'rest_route', FILTER_SANITIZE_URL ) ?? '';
293
294 $is_url_match = preg_match( '/users/i', $request_uri ) !== 0 ||
295 preg_match( '/users/i', $rest_route ) !== 0;
296
297 if (
298 in_array(
299 'enum-rest',
300 $this->get_enabled_user_enums(),
301 true
302 ) &&
303 $is_url_match &&
304 ! is_user_logged_in()
305 ) {
306 return new WP_Error(
307 'rest_cannot_access',
308 esc_html__( 'Only logged-in users can access the User endpoint REST API.', 'defender-security' ),
309 array( 'status' => rest_authorization_required_code() )
310 );
311 }
312
313 return $errors;
314 }
315
316 /**
317 * Block REST user endpoints for not logged-in access.
318 *
319 * @param array $endpoints The available endpoints.
320 *
321 * @return array
322 */
323 public function block_rest_api_user_endpoints( $endpoints ) {
324 if ( is_user_logged_in() ) {
325 return $endpoints;
326 }
327 if ( ! in_array( 'enum-rest', $this->get_enabled_user_enums(), true ) ) {
328 return $endpoints;
329 }
330
331 $endpoints_to_remove = array(
332 '/wp/v2/users',
333 '/wp/v2/users/(?P<id>[\d]+)',
334 );
335
336 foreach ( $endpoints_to_remove as $route ) {
337 if ( isset( $endpoints[ $route ] ) ) {
338 unset( $endpoints[ $route ] );
339 }
340 }
341
342 return $endpoints;
343 }
344
345 /**
346 * Dynamically register rest_prepare_{$post_type} filters for all REST-exposed post types.
347 *
348 * @return void
349 */
350 public function register_dynamic_post_type_filters() {
351 // Optimization: If REST API protection is disabled in the settings, we don't log anything at all.
352 if ( ! in_array( 'enum-rest', $this->get_enabled_user_enums(), true ) ) {
353 return;
354 }
355
356 $post_types = get_post_types( array( 'show_in_rest' => true ) );
357
358 foreach ( $post_types as $post_type ) {
359 add_filter( "rest_prepare_{$post_type}", array( $this, 'strip_author_link' ), 10, 1 );
360 }
361 }
362
363 /**
364 * Remove the 'author' relationship link from REST API responses for not logged-in access.
365 *
366 * @param WP_REST_Response $response The response object.
367 *
368 * @return WP_REST_Response
369 */
370 public function strip_author_link( $response ) {
371 // This is a mandatory check because it is triggered at the moment of data transfer and knows for sure whether the user who sent the request is authorized.
372 if ( ! is_user_logged_in() ) {
373 $response->remove_link( 'author' );
374 }
375
376 return $response;
377 }
378 }
379