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
defender-security / src / component / security-tweaks / class-security-key.php

class-security-key.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/security-tweaks/class-security-key.php

631 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the security key and salt generation for WordPress configuration.
4 *
5 * @package WP_Defender\Component\Security_Tweaks
6 */
7
8 namespace WP_Defender\Component\Security_Tweaks;
9
10 use WP_Error;
11 use Throwable;
12 use Calotes\Component\Response;
13 use WP_Defender\Traits\File_Operations;
14 use WP_Defender\Model\Setting\Mask_Login;
15 use WP_Defender\Model\Setting\Security_Tweaks;
16 use WP_Defender\Traits\Security_Tweaks_Option;
17 use WP_Defender\Component\Network_Cron_Manager;
18 use WP_Defender\Component\Security_Tweak;
19
20 /**
21 * Class Security_Key
22 */
23 class Security_Key extends Abstract_Security_Tweaks implements Security_Key_Const_Interface {
24
25 use File_Operations;
26 use Security_Tweaks_Option;
27
28 public const REGENERATE_SALT_NEXT_RUN_OPTION = 'wpdef_regereate_salt_next_run';
29
30 /**
31 * The slug identifier for the component.
32 *
33 * @var string
34 */
35 public string $slug = 'security-key';
36 /**
37 * Default reminder duration for regenerating security keys.
38 *
39 * @var string
40 */
41 public string $default_days = '60 days';
42 /**
43 * Duration after which a reminder for regeneration is triggered.
44 *
45 * @var string
46 */
47 public string $reminder_duration = '60 days';
48 /**
49 * Date of the last reminder.
50 *
51 * @var int
52 */
53 public int $reminder_date = 0;
54 /**
55 * Timestamp of the last modification of security keys.
56 *
57 * @var int
58 */
59 public int $last_modified = 0;
60 /**
61 * Path to the wp-config.php file.
62 *
63 * @var string
64 */
65 public string $file;
66
67 /**
68 * Flag to automate the security key/salt generation.
69 *
70 * @var bool
71 */
72 private bool $is_autogenerate_keys = true;
73
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 /**
82 * Constructor for Security_Key.
83 */
84 public function __construct() {
85 $this->file = defender_wp_config_path();
86 $this->get_options();
87 $this->cron_schedule();
88 }
89
90 /**
91 * Check whether the issue has been resolved or not.
92 *
93 * @return bool|void
94 */
95 public function check() {
96 if ( $this->is_reverted ) {
97 return false;
98 }
99
100 if ( ! $this->is_salts_exist() ) {
101 return false;
102 }
103
104 if ( $this->last_modified ) {
105 $reminder_date = strtotime( '+' . $this->reminder_duration, $this->last_modified );
106
107 return $reminder_date > defender_get_current_time();
108 }
109 }
110
111 /**
112 * Get options.
113 *
114 * @return void
115 */
116 private function get_options(): void {
117 $options = get_site_option( 'defender_security_tweaks_' . $this->slug );
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'];
121
122 $last_modified = $this->get_wp_config_last_modified_time();
123 if ( false === $last_modified ) {
124 $last_modified = $options['last_modified'] ?? null;
125 } elseif ( isset( $options['last_modified'] ) && is_int( $options['last_modified'] ) && 0 < $options['last_modified'] && $options['last_modified'] < $last_modified ) {
126 $last_modified = $options['last_modified'];
127 }
128 $this->last_modified = $last_modified;
129
130 $this->is_autogenerate_keys = isset( $options['is_autogenerate_keys'] );
131 }
132
133 /**
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.
135 *
136 * @return bool|WP_Error|Response
137 */
138 public function process() {
139 $is_done = $this->update_keys();
140
141 if ( is_wp_error( $is_done ) ) {
142 return $is_done;
143 }
144
145 if ( $is_done ) {
146 $mask_login = wd_di()->get( Mask_Login::class );
147 $url = $mask_login->is_active()
148 ? $mask_login->get_new_login_url()
149 : wp_login_url( network_admin_url( 'admin.php?page=wdf-hardener' ) );
150
151 $interval = 3;
152
153 return new Response(
154 true,
155 array(
156 'message' => sprintf(
157 /* translators: 1: login link, 2: line break, 3: timer. */
158 esc_html__(
159 'All key salts have been regenerated. You will now need to %1$s. %2$s This will auto reload after %3$s seconds.',
160 'defender-security'
161 ),
162 '<a href="' . $url . '"><strong>' . esc_html__( 're-login', 'defender-security' ) . '</strong></a>',
163 '<br>',
164 '<span class="hardener-timer">' . $interval . '</span>'
165 ),
166 'redirect' => $url,
167 'interval' => $interval,
168 )
169 );
170 }
171
172 return $is_done;
173 }
174
175 /**
176 * This is for un-do stuff that has be done in @process.
177 *
178 * @return bool
179 */
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
186 return true;
187 }
188
189 /**
190 * Shield up.
191 *
192 * @return bool
193 */
194 public function shield_up(): bool {
195 return true;
196 }
197
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 /**
247 * Get salts to be placed in wp-config.php.
248 *
249 * @return array|WP_Error
250 */
251 private function get_salts(): WP_Error|array {
252 $constants = $this->get_constants();
253 $max_attempts = 3;
254 $attempt = 0;
255
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 }
268 }
269
270 return new WP_Error(
271 'defender_salts_not_found',
272 esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' )
273 );
274 }
275
276 /**
277 * Get how long the wp-config file is last updated.
278 *
279 * @return int|string
280 */
281 private function get_last_modified_days(): int|string {
282 $current_time = defender_get_current_time();
283 $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS;
284 return $days_ago ? (int) round( $days_ago ) : 'unknown';
285 }
286
287 /**
288 * Get all the constants.
289 *
290 * @return array
291 */
292 private function get_constants(): array {
293 return array(
294 'AUTH_KEY',
295 'SECURE_AUTH_KEY',
296 'LOGGED_IN_KEY',
297 'NONCE_KEY',
298 'AUTH_SALT',
299 'SECURE_AUTH_SALT',
300 'LOGGED_IN_SALT',
301 'NONCE_SALT',
302 );
303 }
304
305 /**
306 * Retrieve the tweak's label.
307 *
308 * @return string
309 */
310 public function get_label(): string {
311 return esc_html__( 'Update security keys', 'defender-security' );
312 }
313
314 /**
315 * Get the error reason.
316 *
317 * @return string
318 */
319 public function get_error_reason(): string {
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 )
327 );
328 }
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 )
338 );
339 }
340
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 );
352 }
353
354 /**
355 * Return a summary data of this tweak.
356 *
357 * @return array
358 */
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
369 return array(
370 'slug' => $this->slug,
371 'title' => $this->get_label(),
372 'errorReason' => $this->get_error_reason(),
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 )
380 ),
381 'misc' => array(
382 'show_revert_button' => true,
383 'reminder' => $reminder_duration,
384 'show_action_button' => true,
385 ),
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' ) ),
388 );
389 }
390
391 /**
392 * Getter method of is_autogenerate_keys.
393 *
394 * @return bool. Return true or false which is used to trigger auto generation of security salt/key.
395 */
396 public function get_is_autogenerate_keys(): bool {
397 $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' );
398
399 $this->is_autogenerate_keys = is_null( $is_autogenerate_keys ) || (bool) $is_autogenerate_keys;
400
401 return $this->is_autogenerate_keys;
402 }
403
404 /**
405 * Setter method of is_autogenerate_keys.
406 *
407 * @param bool $value Boolean flag of the is_autogenerate_keys.
408 *
409 * @return bool Return true if value updated, otherwise false.
410 */
411 public function set_is_autogenrate_keys( bool $value ): bool {
412 $this->is_autogenerate_keys = $value;
413
414 return $this->update_option(
415 'is_autogenerate_keys',
416 $this->is_autogenerate_keys
417 );
418 }
419
420 /**
421 * Cron schedule.
422 *
423 * @return void
424 */
425 public function cron_schedule(): void {
426 if (
427 true === $this->get_is_autogenerate_keys()
428 ) {
429 $display_name = $this->get_option( 'reminder_duration' );
430
431 if ( ! is_string( $display_name ) || '' === $display_name ) {
432 $display_name = $this->default_days;
433 }
434
435 /**
436 * Network Cron Manager instance.
437 *
438 * @var Network_Cron_Manager $network_cron_manager
439 */
440 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
441
442 $interval_seconds = strtotime( $display_name, 0 );
443 $start_time = defender_get_current_time() + $interval_seconds;
444
445 $network_cron_manager->register_callback(
446 'wpdef_sec_key_gen',
447 array( $this, 'cron_process' ),
448 $interval_seconds,
449 $start_time,
450 );
451
452 }
453 }
454
455 /**
456 * Cron unscheduled.
457 *
458 * @return void
459 */
460 public function cron_unschedule(): void {
461 /**
462 * Network Cron Manager instance.
463 *
464 * @var Network_Cron_Manager $network_cron_manager
465 */
466 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
467 $network_cron_manager->remove_callback( 'wpdef_sec_key_gen' );
468 }
469
470 /**
471 * Cron schedule security key/salt generation process.
472 *
473 * @return void
474 */
475 public function cron_process(): void {
476 try {
477 $security_tweak_model = new Security_Tweaks();
478 if ( ! $security_tweak_model->is_tweak_ignore( $this->slug ) &&
479 true === $this->get_is_autogenerate_keys()
480 ) {
481 $this->update_keys();
482 }
483 } catch ( Throwable $th ) {
484 $this->log( 'Security Key. Cron Throwable: ' . get_class( $th ) . ': ' . $th->getMessage(), Security_Tweak::LOG_FILE_NAME );
485 }
486 }
487
488 /**
489 * Check all salts are exists.
490 *
491 * @return bool Return true if all constants have salt else false.
492 */
493 private function is_salts_exist(): bool {
494 foreach ( $this->get_constants() as $constants ) {
495 if ( ! defined( $constants ) ) {
496 return false;
497 }
498 }
499
500 return true;
501 }
502
503 /**
504 * Append salt to wp-config.php file content string.
505 *
506 * @param bool|string $contents Text content of wp-config.php.
507 * @param string $new_salts Salts need to be appended.
508 *
509 * @return string Text content after salts appended.
510 */
511 private function append_salts( bool|string $contents, string $new_salts ): string {
512 $pattern = '/(define\(\s*\'AUTH_KEY\'.*?\);)\s*' .
513 '(define\(\s*\'SECURE_AUTH_KEY\'.*?\);)\s*' .
514 '(define\(\s*\'LOGGED_IN_KEY\'.*?\);)\s*' .
515 '(define\(\s*\'NONCE_KEY\'.*?\);)\s*' .
516 '(define\(\s*\'AUTH_SALT\'.*?\);)\s*' .
517 '(define\(\s*\'SECURE_AUTH_SALT\'.*?\);)\s*' .
518 '(define\(\s*\'LOGGED_IN_SALT\'.*?\);)\s*' .
519 '(define\(\s*\'NONCE_SALT\'.*?\);)/s';
520
521 if ( preg_match( $pattern, $contents ) ) {
522 return preg_replace( $pattern, $new_salts, $contents );
523 } else {
524 return preg_replace( '/(define\(\s*\'DB_NAME\'\s*,.*?\);)/s', $new_salts . PHP_EOL . '$1', $contents );
525 }
526 }
527
528 /**
529 * Return collection of reminder frequencies.
530 *
531 * @return array
532 */
533 public function reminder_frequencies(): array {
534 return array(
535 '30 days',
536 '60 days',
537 '90 days',
538 '6 months',
539 '1 year',
540 );
541 }
542
543 /**
544 * Get the last modified time of wp-config file.
545 *
546 * @return int|false
547 * @since 3.10.0
548 */
549 private function get_wp_config_last_modified_time(): bool|int {
550 $file = $this->file;
551 if ( ! file_exists( $file ) ) {
552 $file = ABSPATH . WPINC . '/general-template.php';
553 }
554
555 return filemtime( $file );
556 }
557
558 /**
559 * Update keys.
560 *
561 * @return WP_Error|bool|array True if success. WP_Error if any error.
562 */
563 private function update_keys(): WP_Error|bool|array {
564 $wp_filesystem = $this->get_wp_filesystem();
565
566 if ( ! $wp_filesystem->is_writable( $this->file ) ) {
567 return new WP_Error(
568 'defender_file_not_writable',
569 /* translators: %s: file path */
570 sprintf( esc_html__( 'The file %s is not writable', 'defender-security' ), $this->file )
571 );
572 }
573
574 $constants = $this->get_constants();
575 $salts = $this->get_salts();
576
577 if ( is_wp_error( $salts ) ) {
578 return $salts;
579 }
580
581 $contents = $wp_filesystem->get_contents( $this->file );
582
583 $new_salts = '';
584
585 foreach ( $constants as $key => $const ) {
586 if ( defined( $const ) ) {
587 $pattern = "/^define\(\s*['|\"]{$const}['|\"],(.*)\)\s*;/m";
588 $replacement = $salts[ $key ];
589 $contents = preg_replace_callback(
590 $pattern,
591 function () use ( $replacement ) {
592 return $replacement;
593 },
594 $contents
595 );
596 } else {
597 $new_salts .= $salts[ $key ] . PHP_EOL;
598 }
599 }
600
601 if ( '' !== $new_salts ) {
602 $new_salts = PHP_EOL .
603 '/* DEFENDER GENERATED SALTS */' .
604 PHP_EOL .
605 $new_salts .
606 PHP_EOL;
607
608 $contents = $this->append_salts( $contents, $new_salts );
609 }
610
611 $is_done = $wp_filesystem->put_contents( $this->file, $contents, FS_CHMOD_FILE );
612
613 if ( ! $is_done ) {
614 return false;
615 }
616 $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
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;
621 update_site_option( 'defender_security_tweaks_' . $this->slug, $values );
622 $this->log( 'Security keys are updated.', Security_Tweak::LOG_FILE_NAME );
623 if ( is_multisite() && ! headers_sent() ) {
624 // Delete cookies forced.
625 wp_clear_auth_cookie();
626 }
627
628 return true;
629 }
630 }
631