PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
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
← All changes | src/component/security-tweaks/class-security-key.php +168 -79 5.7.2 → 6.3.0 View file →
@@ -7,17 +7,16 @@
7 7
8 8 namespace WP_Defender\Component\Security_Tweaks;
9 9
10 10 use WP_Error;
11 -use Exception;
12 11 use Throwable;
13 12 use Calotes\Component\Response;
13 +use WP_Defender\Traits\File_Operations;
14 14 use WP_Defender\Model\Setting\Mask_Login;
15 15 use WP_Defender\Model\Setting\Security_Tweaks;
16 16 use WP_Defender\Traits\Security_Tweaks_Option;
17 17 use WP_Defender\Component\Network_Cron_Manager;
18 18 use WP_Defender\Component\Security_Tweak;
19 -use WP_Filesystem_Base;
20 19
21 20 /**
22 21 * Class Security_Key
23 22 */
@@ -22,8 +21,9 @@
22 21 * Class Security_Key
23 22 */
24 23 class Security_Key extends Abstract_Security_Tweaks implements Security_Key_Const_Interface {
25 24
25 + use File_Operations;
26 26 use Security_Tweaks_Option;
27 27
28 28 public const REGENERATE_SALT_NEXT_RUN_OPTION = 'wpdef_regereate_salt_next_run';
29 29
@@ -37,33 +37,33 @@
37 37 * Default reminder duration for regenerating security keys.
38 38 *
39 39 * @var string
40 40 */
41 - public $default_days = '60 days';
41 + public string $default_days = '60 days';
42 42 /**
43 43 * Duration after which a reminder for regeneration is triggered.
44 44 *
45 - * @var null
45 + * @var string
46 46 */
47 - public $reminder_duration = null;
47 + public string $reminder_duration = '60 days';
48 48 /**
49 49 * Date of the last reminder.
50 50 *
51 - * @var null
51 + * @var int
52 52 */
53 - public $reminder_date = null;
53 + public int $reminder_date = 0;
54 54 /**
55 55 * Timestamp of the last modification of security keys.
56 56 *
57 - * @var null
57 + * @var int
58 58 */
59 - public $last_modified = null;
59 + public int $last_modified = 0;
60 60 /**
61 61 * Path to the wp-config.php file.
62 62 *
63 63 * @var string
64 64 */
65 - public $file;
65 + public string $file;
66 66
67 67 /**
68 68 * Flag to automate the security key/salt generation.
69 69 *
@@ -68,11 +68,18 @@
68 68 * Flag to automate the security key/salt generation.
69 69 *
70 70 * @var bool
71 71 */
72 - private $is_autogenerate_keys = true;
72 + private bool $is_autogenerate_keys = true;
73 73
74 74 /**
75 + * Flag set when the user explicitly reverts this tweak via the Edit button.
76 + *
77 + * @var bool
78 + */
79 + private bool $is_reverted = false;
80 +
81 + /**
75 82 * Constructor for Security_Key.
76 83 */
77 84 public function __construct() {
78 85 $this->file = defender_wp_config_path();
@@ -85,8 +92,12 @@
85 92 *
86 93 * @return bool|void
87 94 */
88 95 public function check() {
96 + if ( $this->is_reverted ) {
97 + return false;
98 + }
99 +
89 100 if ( ! $this->is_salts_exist() ) {
90 101 return false;
91 102 }
92 103
@@ -92,9 +103,9 @@
92 103
93 104 if ( $this->last_modified ) {
94 105 $reminder_date = strtotime( '+' . $this->reminder_duration, $this->last_modified );
95 106
96 - return $reminder_date > time();
107 + return $reminder_date > defender_get_current_time();
97 108 }
98 109 }
99 110
100 111 /**
@@ -103,22 +114,21 @@
103 114 * @return void
104 115 */
105 116 private function get_options(): void {
106 117 $options = get_site_option( 'defender_security_tweaks_' . $this->slug );
107 - $this->reminder_date = ! empty( $options['reminder_date'] ) ? $options['reminder_date'] : null;
108 - $this->reminder_duration = ! empty( $options['reminder_duration'] ) ? $options['reminder_duration'] : $this->default_days;
118 + $this->reminder_date = $options['reminder_date'] ?? 0;
119 + $this->reminder_duration = isset( $options['reminder_duration'] ) && '' !== $options['reminder_duration'] ? $options['reminder_duration'] : $this->default_days;
120 + $this->is_reverted = isset( $options['is_reverted'] ) && true === (bool) $options['is_reverted'];
109 121
110 122 $last_modified = $this->get_wp_config_last_modified_time();
111 123 if ( false === $last_modified ) {
112 124 $last_modified = $options['last_modified'] ?? null;
113 - } elseif ( ! empty( $options['last_modified'] ) && $options['last_modified'] < $last_modified ) {
125 + } elseif ( isset( $options['last_modified'] ) && is_int( $options['last_modified'] ) && 0 < $options['last_modified'] && $options['last_modified'] < $last_modified ) {
114 126 $last_modified = $options['last_modified'];
115 127 }
116 128 $this->last_modified = $last_modified;
117 129
118 - $this->is_autogenerate_keys = ! empty( $options['is_autogenerate_keys'] )
119 - ? (bool) $options['is_autogenerate_keys']
120 - : true;
130 + $this->is_autogenerate_keys = isset( $options['is_autogenerate_keys'] );
121 131 }
122 132
123 133 /**
124 134 * Here is the code for processing. If the return is true or Response, we add it to resolve list. WP_Error if any error.
@@ -167,8 +177,13 @@
167 177 *
168 178 * @return bool
169 179 */
170 180 public function revert(): bool {
181 + $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
182 + $values['is_reverted'] = true;
183 + update_site_option( 'defender_security_tweaks_' . $this->slug, $values );
184 + $this->is_reverted = true;
185 +
171 186 return true;
172 187 }
173 188
174 189 /**
@@ -180,23 +195,83 @@
180 195 return true;
181 196 }
182 197
183 198 /**
199 + * Parse and validate salt definitions string.
200 + *
201 + * @param string $body API response body.
202 + * @param array $constants Expected constant keys.
203 + *
204 + * @return array|false
205 + */
206 + private function parse_and_validate_salts( string $body, array $constants ): array|false {
207 + $raw_salts = explode( "\n", $body );
208 + $salts = array();
209 + $values = array();
210 + $index = 0;
211 +
212 + foreach ( $raw_salts as $salt ) {
213 + $salt = trim( $salt );
214 + if ( '' === $salt ) {
215 + continue;
216 + }
217 +
218 + $salt = stripslashes( $salt );
219 + $expected_const = $constants[ $index ] ?? null;
220 +
221 + if (
222 + null === $expected_const ||
223 + 0 !== strpos( $salt, 'define(' ) ||
224 + 1 !== preg_match( '/^define\(\s*[\'"]' . preg_quote( $expected_const, '/' ) . '[\'"]\s*,\s*[\'"](.*?)[\'"]\s*\)\s*;$/s', $salt, $matches )
225 + ) {
226 + return false;
227 + }
228 +
229 + $val = $matches[1];
230 + if ( 'put your unique phrase here' === $val || strlen( $val ) < 64 || isset( $values[ $val ] ) ) {
231 + return false;
232 + }
233 +
234 + $values[ $val ] = true;
235 + $salts[] = $salt;
236 + ++$index;
237 + }
238 +
239 + if ( count( $salts ) !== count( $constants ) ) {
240 + return false;
241 + }
242 +
243 + return $salts;
244 + }
245 +
246 + /**
184 247 * Get salts to be placed in wp-config.php.
185 248 *
186 249 * @return array|WP_Error
187 250 */
188 - private function get_salts() {
189 - $response = wp_safe_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' );
251 + private function get_salts(): WP_Error|array {
252 + $constants = $this->get_constants();
253 + $max_attempts = 3;
254 + $attempt = 0;
190 255
191 - if ( is_wp_error( $response ) ) {
192 - return new WP_Error(
193 - 'defender_salts_not_found',
194 - esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' )
195 - );
256 + while ( $attempt < $max_attempts ) {
257 + ++$attempt;
258 + $response = wp_safe_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' );
259 +
260 + if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
261 + continue;
262 + }
263 +
264 + $salts = $this->parse_and_validate_salts( wp_remote_retrieve_body( $response ), $constants );
265 + if ( false !== $salts ) {
266 + return $salts;
267 + }
196 268 }
197 269
198 - return array_filter( explode( "\n", wp_remote_retrieve_body( $response ) ) );
270 + return new WP_Error(
271 + 'defender_salts_not_found',
272 + esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' )
273 + );
199 274 }
200 275
201 276 /**
202 277 * Get how long the wp-config file is last updated.
@@ -202,14 +277,12 @@
202 277 * Get how long the wp-config file is last updated.
203 278 *
204 279 * @return int|string
205 280 */
206 - private function get_last_modified_days() {
207 - $current_time = time();
281 + private function get_last_modified_days(): int|string {
282 + $current_time = defender_get_current_time();
208 283 $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS;
209 - $days_ago = $days_ago ? (int) round( $days_ago ) : 'unknown';
210 -
211 - return $days_ago ?? 1;
284 + return $days_ago ? (int) round( $days_ago ) : 'unknown';
212 285 }
213 286
214 287 /**
215 288 * Get all the constants.
@@ -234,9 +307,9 @@
234 307 *
235 308 * @return string
236 309 */
237 310 public function get_label(): string {
238 - return esc_html__( 'Update old security keys', 'defender-security' );
311 + return esc_html__( 'Update security keys', 'defender-security' );
239 312 }
240 313
241 314 /**
242 315 * Get the error reason.
@@ -243,30 +316,40 @@
243 316 *
244 317 * @return string
245 318 */
246 319 public function get_error_reason(): string {
247 - $get_last_modified_days = $this->get_last_modified_days();
248 -
249 - if ( 'unknown' === $get_last_modified_days ) {
250 - $error_message = esc_html__(
251 - 'We can\'t tell how old your security keys are, perhaps it\'s time to update them?',
252 - 'defender-security'
320 + if ( ! $this->is_salts_exist() ) {
321 + return wp_kses(
322 + __( '<strong>Your security keys are outdated</strong> <br /> and one or more security salts are missing in wp-config.php.', 'defender-security' ),
323 + array(
324 + 'strong' => array(),
325 + 'br' => array(),
326 + )
253 327 );
254 328 }
255 - if ( ! $this->is_salts_exist() ) {
256 - $error_message = esc_html__(
257 - 'One or more security salts aren\'t defined in wp-config.php. Time to regenerate them!',
258 - 'defender-security'
329 +
330 + $days = $this->get_last_modified_days();
331 + if ( 'unknown' === $days ) {
332 + return wp_kses(
333 + __( '<strong>Your security keys are outdated</strong> <br /> and we can\'t tell how old they are.', 'defender-security' ),
334 + array(
335 + 'strong' => array(),
336 + 'br' => array(),
337 + )
259 338 );
260 - } else {
261 - $error_message = sprintf(
262 - /* translators: %s: number of days */
263 - esc_html__( 'Your current security keys are %s days old. Time to update them!', 'defender-security' ),
264 - $get_last_modified_days
265 - );
266 339 }
267 340
268 - return $error_message;
341 + return wp_kses(
342 + sprintf(
343 + /* translators: %1$s: opening strong tag, %2$s: closing strong tag */
344 + __( '%1$sYour security keys are outdated.%2$s', 'defender-security' ),
345 + '<strong>',
346 + '</strong>'
347 + ),
348 + array(
349 + 'strong' => array(),
350 + )
351 + );
269 352 }
270 353
271 354 /**
272 355 * Return a summary data of this tweak.
@@ -273,25 +356,36 @@
273 356 *
274 357 * @return array
275 358 */
276 359 public function to_array(): array {
360 + $auto_regenerate = $this->get_is_autogenerate_keys();
361 + $auto_status = $auto_regenerate
362 + ? __( 'Auto-regenerate is turned on.', 'defender-security' )
363 + : __( 'Auto-regenerate is turned off.', 'defender-security' );
364 + $saved_duration = $this->get_option( 'reminder_duration' );
365 + $reminder_duration = ( is_string( $saved_duration ) && '' !== $saved_duration )
366 + ? $saved_duration
367 + : $this->default_days;
368 +
277 369 return array(
278 370 'slug' => $this->slug,
279 371 'title' => $this->get_label(),
280 372 'errorReason' => $this->get_error_reason(),
281 - 'successReason' => sprintf(
282 - /* translators: %s: number of days */
283 - esc_html__( 'Your security keys are less than %s days old, nice work.', 'defender-security' ),
284 - $this->get_last_modified_days()
373 + 'successReason' => wp_strip_all_tags(
374 + sprintf(
375 + /* translators: %1$s: reminder duration, %2$s: auto-regenerate status */
376 + __( 'Security keys regenerate every %1$s. %2$s', 'defender-security' ),
377 + $reminder_duration,
378 + $auto_status
379 + )
285 380 ),
286 381 'misc' => array(
287 - 'reminder' => $this->reminder_duration,
382 + 'show_revert_button' => true,
383 + 'reminder' => $reminder_duration,
384 + 'show_action_button' => true,
288 385 ),
289 - 'bulk_description' => esc_html__(
290 - 'Your current security keys are unknown days old. Time to update them! We will update the frequency to 60 days.',
291 - 'defender-security'
292 - ),
293 - 'bulk_title' => esc_html__( 'Security Keys', 'defender-security' ),
386 + 'bulk_description' => wp_strip_all_tags( __( 'WordPress security keys help protect user sessions and cookies. Update security keys with strong, unique values to improve encryption and reduce the risk of unauthorized access.', 'defender-security' ) ),
387 + 'bulk_title' => wp_strip_all_tags( __( 'Security Keys', 'defender-security' ) ),
294 388 );
295 389 }
296 390
297 391 /**
@@ -301,11 +395,9 @@
301 395 */
302 396 public function get_is_autogenerate_keys(): bool {
303 397 $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' );
304 398
305 - $this->is_autogenerate_keys = is_null( $is_autogenerate_keys )
306 - ? true
307 - : (bool) $is_autogenerate_keys;
399 + $this->is_autogenerate_keys = is_null( $is_autogenerate_keys ) || (bool) $is_autogenerate_keys;
308 400
309 401 return $this->is_autogenerate_keys;
310 402 }
311 403
@@ -316,9 +408,9 @@
316 408 *
317 409 * @return bool Return true if value updated, otherwise false.
318 410 */
319 411 public function set_is_autogenrate_keys( bool $value ): bool {
320 - $this->is_autogenerate_keys = (bool) $value;
412 + $this->is_autogenerate_keys = $value;
321 413
322 414 return $this->update_option(
323 415 'is_autogenerate_keys',
324 416 $this->is_autogenerate_keys
@@ -335,9 +427,9 @@
335 427 true === $this->get_is_autogenerate_keys()
336 428 ) {
337 429 $display_name = $this->get_option( 'reminder_duration' );
338 430
339 - if ( empty( $display_name ) ) {
431 + if ( ! is_string( $display_name ) || '' === $display_name ) {
340 432 $display_name = $this->default_days;
341 433 }
342 434
343 435 /**
@@ -347,9 +439,9 @@
347 439 */
348 440 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
349 441
350 442 $interval_seconds = strtotime( $display_name, 0 );
351 - $start_time = time() + $interval_seconds;
443 + $start_time = defender_get_current_time() + $interval_seconds;
352 444
353 445 $network_cron_manager->register_callback(
354 446 'wpdef_sec_key_gen',
355 447 array( $this, 'cron_process' ),
@@ -410,14 +502,14 @@
410 502
411 503 /**
412 504 * Append salt to wp-config.php file content string.
413 505 *
414 - * @param string|false $contents Text content of wp-config.php.
415 - * @param string $new_salts Salts need to be appended.
506 + * @param bool|string $contents Text content of wp-config.php.
507 + * @param string $new_salts Salts need to be appended.
416 508 *
417 509 * @return string Text content after salts appended.
418 510 */
419 - private function append_salts( $contents, string $new_salts ): string {
511 + private function append_salts( bool|string $contents, string $new_salts ): string {
420 512 $pattern = '/(define\(\s*\'AUTH_KEY\'.*?\);)\s*' .
421 513 '(define\(\s*\'SECURE_AUTH_KEY\'.*?\);)\s*' .
422 514 '(define\(\s*\'LOGGED_IN_KEY\'.*?\);)\s*' .
423 515 '(define\(\s*\'NONCE_KEY\'.*?\);)\s*' .
@@ -453,9 +545,9 @@
453 545 *
454 546 * @return int|false
455 547 * @since 3.10.0
456 548 */
457 - private function get_wp_config_last_modified_time() {
549 + private function get_wp_config_last_modified_time(): bool|int {
458 550 $file = $this->file;
459 551 if ( ! file_exists( $file ) ) {
460 552 $file = ABSPATH . WPINC . '/general-template.php';
461 553 }
@@ -468,15 +560,10 @@
468 560 *
469 561 * @return WP_Error|bool|array True if success. WP_Error if any error.
470 562 */
471 563 private function update_keys(): WP_Error|bool|array {
472 - global $wp_filesystem;
564 + $wp_filesystem = $this->get_wp_filesystem();
473 565
474 - if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
475 - require_once ABSPATH . '/wp-admin/includes/file.php';
476 - WP_Filesystem();
477 - }
478 -
479 566 if ( ! $wp_filesystem->is_writable( $this->file ) ) {
480 567 return new WP_Error(
481 568 'defender_file_not_writable',
482 569 /* translators: %s: file path */
@@ -510,9 +597,9 @@
510 597 $new_salts .= $salts[ $key ] . PHP_EOL;
511 598 }
512 599 }
513 600
514 - if ( ! empty( $new_salts ) ) {
601 + if ( '' !== $new_salts ) {
515 602 $new_salts = PHP_EOL .
516 603 '/* DEFENDER GENERATED SALTS */' .
517 604 PHP_EOL .
518 605 $new_salts .
@@ -520,16 +607,18 @@
520 607
521 608 $contents = $this->append_salts( $contents, $new_salts );
522 609 }
523 610
524 - $is_done = (bool) file_put_contents( $this->file, $contents, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
611 + $is_done = $wp_filesystem->put_contents( $this->file, $contents, FS_CHMOD_FILE );
525 612
526 613 if ( ! $is_done ) {
527 614 return false;
528 615 }
529 616 $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
530 - $this->last_modified = time();
531 - $values['last_modified'] = time();
617 + $this->last_modified = defender_get_current_time();
618 + $values['last_modified'] = defender_get_current_time();
619 + unset( $values['is_reverted'] );
620 + $this->is_reverted = false;
532 621 update_site_option( 'defender_security_tweaks_' . $this->slug, $values );
533 622 $this->log( 'Security keys are updated.', Security_Tweak::LOG_FILE_NAME );
534 623 if ( is_multisite() && ! headers_sent() ) {
535 624 // Delete cookies forced.