PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.0
Defender Security – Malware Scanner, Login Security & Firewall v5.6.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 5.6.0, at src/component/security-tweaks/class-security-key.php

582 lines 14.5 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 Exception;
12 use Throwable;
13 use Calotes\Component\Response;
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\Security_Tweak;
18 use WP_Filesystem_Base;
19
20 /**
21 * Class Security_Key
22 */
23 class Security_Key extends Abstract_Security_Tweaks implements Security_Key_Const_Interface {
24
25 use Security_Tweaks_Option;
26
27 public const REGENERATE_SALT_NEXT_RUN_OPTION = 'wpdef_regereate_salt_next_run';
28
29 /**
30 * The slug identifier for the component.
31 *
32 * @var string
33 */
34 public string $slug = 'security-key';
35 /**
36 * Default reminder duration for regenerating security keys.
37 *
38 * @var string
39 */
40 public $default_days = '60 days';
41 /**
42 * Duration after which a reminder for regeneration is triggered.
43 *
44 * @var null
45 */
46 public $reminder_duration = null;
47 /**
48 * Date of the last reminder.
49 *
50 * @var null
51 */
52 public $reminder_date = null;
53 /**
54 * Timestamp of the last modification of security keys.
55 *
56 * @var null
57 */
58 public $last_modified = null;
59 /**
60 * Path to the wp-config.php file.
61 *
62 * @var string
63 */
64 public $file;
65
66 /**
67 * Flag to automate the security key/salt generation.
68 *
69 * @var bool
70 */
71 private $is_autogenerate_keys = true;
72
73 /**
74 * Constructor for Security_Key.
75 */
76 public function __construct() {
77 $this->file = defender_wp_config_path();
78 $this->add_hooks();
79 $this->get_options();
80 $this->cron_schedule();
81 }
82
83 /**
84 * Check whether the issue has been resolved or not.
85 *
86 * @return bool|void
87 */
88 public function check() {
89 if ( ! $this->is_salts_exist() ) {
90 return false;
91 }
92
93 if ( $this->last_modified ) {
94 $reminder_date = strtotime( '+' . $this->reminder_duration, $this->last_modified );
95
96 return $reminder_date > time();
97 }
98 }
99
100 /**
101 * Get options.
102 *
103 * @return void
104 */
105 private function get_options(): void {
106 $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;
109
110 $last_modified = $this->get_wp_config_last_modified_time();
111 if ( false === $last_modified ) {
112 $last_modified = $options['last_modified'] ?? null;
113 } elseif ( ! empty( $options['last_modified'] ) && $options['last_modified'] < $last_modified ) {
114 $last_modified = $options['last_modified'];
115 }
116 $this->last_modified = $last_modified;
117
118 $this->is_autogenerate_keys = ! empty( $options['is_autogenerate_keys'] )
119 ? (bool) $options['is_autogenerate_keys']
120 : true;
121 }
122
123 /**
124 * Here is the code for processing. If the return is true or Response, we add it to resolve list. WP_Error if any error.
125 *
126 * @return bool|WP_Error|Response
127 */
128 public function process() {
129 global $wp_filesystem;
130 // Initialize the WP filesystem, no more using 'file-put-contents' function.
131 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
132 require_once ABSPATH . '/wp-admin/includes/file.php';
133 WP_Filesystem();
134 }
135 if ( ! $wp_filesystem->is_writable( $this->file ) ) {
136 return new WP_Error(
137 'defender_file_not_writable',
138 /* translators: %s: file path */
139 sprintf( esc_html__( 'The file %s is not writable', 'defender-security' ), $this->file )
140 );
141 }
142
143 $constants = $this->get_constants();
144 $salts = $this->get_salts();
145
146 if ( is_wp_error( $salts ) ) {
147 return $salts;
148 }
149
150 $contents = $wp_filesystem->get_contents( $this->file );
151 $new_salts = '';
152
153 foreach ( $constants as $key => $const ) {
154 if ( defined( $const ) ) {
155 $pattern = "/^define\(\s*['|\"]{$const}['|\"],(.*)\)\s*;/m";
156 $replacement = $salts[ $key ];
157 $contents = preg_replace_callback(
158 $pattern,
159 function () use ( $replacement ) {
160 return $replacement;
161 },
162 $contents
163 );
164 } else {
165 $new_salts .= $salts[ $key ] . PHP_EOL;
166 }
167 }
168
169 if ( ! empty( $new_salts ) ) {
170 $new_salts = PHP_EOL .
171 '/* DEFENDER GENERATED SALTS */' .
172 PHP_EOL .
173 $new_salts .
174 PHP_EOL;
175
176 $contents = $this->append_salts( $contents, $new_salts );
177 }
178
179 $is_done = (bool) file_put_contents( $this->file, $contents, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
180
181 if ( $is_done ) {
182 $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
183 $this->last_modified = time();
184 $values['last_modified'] = time();
185 update_site_option( 'defender_security_tweaks_' . $this->slug, $values );
186 $this->log( 'Security keys are updated.', Security_Tweak::LOG_FILE_NAME );
187
188 $mask_login = wd_di()->get( Mask_Login::class );
189 $url = $mask_login->is_active()
190 ? $mask_login->get_new_login_url()
191 : wp_login_url( network_admin_url( 'admin.php?page=wdf-hardener' ) );
192
193 $interval = 3;
194 if ( is_multisite() ) {
195 // Delete cookies forced.
196 wp_clear_auth_cookie();
197 }
198
199 return new Response(
200 true,
201 array(
202 'message' => sprintf(
203 /* translators: 1: login link, 2: line break, 3: timer. */
204 esc_html__(
205 'All key salts have been regenerated. You will now need to %1$s. %2$s This will auto reload after %3$s seconds.',
206 'defender-security'
207 ),
208 '<a href="' . $url . '"><strong>' . esc_html__( 're-login', 'defender-security' ) . '</strong></a>',
209 '<br>',
210 '<span class="hardener-timer">' . $interval . '</span>'
211 ),
212 'redirect' => $url,
213 'interval' => $interval,
214 )
215 );
216 }
217
218 return $is_done;
219 }
220
221 /**
222 * This is for un-do stuff that has be done in @process.
223 *
224 * @return bool
225 */
226 public function revert(): bool {
227 return true;
228 }
229
230 /**
231 * Shield up.
232 *
233 * @return bool
234 */
235 public function shield_up(): bool {
236 return true;
237 }
238
239 /**
240 * Get salts to be placed in wp-config.php.
241 *
242 * @return array|WP_Error
243 */
244 private function get_salts() {
245 $response = wp_safe_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' );
246
247 if ( is_wp_error( $response ) ) {
248 return new WP_Error(
249 'defender_salts_not_found',
250 esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' )
251 );
252 }
253
254 return array_filter( explode( "\n", wp_remote_retrieve_body( $response ) ) );
255 }
256
257 /**
258 * Get how long the wp-config file is last updated.
259 *
260 * @return int|string
261 */
262 private function get_last_modified_days() {
263 $current_time = time();
264 $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS;
265 $days_ago = $days_ago ? (int) round( $days_ago ) : 'unknown';
266
267 return $days_ago ?? 1;
268 }
269
270 /**
271 * Get all the constants.
272 *
273 * @return array
274 */
275 private function get_constants(): array {
276 return array(
277 'AUTH_KEY',
278 'SECURE_AUTH_KEY',
279 'LOGGED_IN_KEY',
280 'NONCE_KEY',
281 'AUTH_SALT',
282 'SECURE_AUTH_SALT',
283 'LOGGED_IN_SALT',
284 'NONCE_SALT',
285 );
286 }
287
288 /**
289 * Retrieve the tweak's label.
290 *
291 * @return string
292 */
293 public function get_label(): string {
294 return esc_html__( 'Update old security keys', 'defender-security' );
295 }
296
297 /**
298 * Get the error reason.
299 *
300 * @return string
301 */
302 public function get_error_reason(): string {
303 $get_last_modified_days = $this->get_last_modified_days();
304
305 if ( 'unknown' === $get_last_modified_days ) {
306 $error_message = esc_html__(
307 'We can\'t tell how old your security keys are, perhaps it\'s time to update them?',
308 'defender-security'
309 );
310 }
311 if ( ! $this->is_salts_exist() ) {
312 $error_message = esc_html__(
313 'One or more security salts aren\'t defined in wp-config.php. Time to regenerate them!',
314 'defender-security'
315 );
316 } else {
317 $error_message = sprintf(
318 /* translators: %s: number of days */
319 esc_html__( 'Your current security keys are %s days old. Time to update them!', 'defender-security' ),
320 $get_last_modified_days
321 );
322 }
323
324 return $error_message;
325 }
326
327 /**
328 * Return a summary data of this tweak.
329 *
330 * @return array
331 */
332 public function to_array(): array {
333 return array(
334 'slug' => $this->slug,
335 'title' => $this->get_label(),
336 'errorReason' => $this->get_error_reason(),
337 'successReason' => sprintf(
338 /* translators: %s: number of days */
339 esc_html__( 'Your security keys are less than %s days old, nice work.', 'defender-security' ),
340 $this->get_last_modified_days()
341 ),
342 'misc' => array(
343 'reminder' => $this->reminder_duration,
344 ),
345 'bulk_description' => esc_html__(
346 'Your current security keys are unknown days old. Time to update them! We will update the frequency to 60 days.',
347 'defender-security'
348 ),
349 'bulk_title' => esc_html__( 'Security Keys', 'defender-security' ),
350 );
351 }
352
353 /**
354 * Getter method of is_autogenerate_keys.
355 *
356 * @return bool. Return true or false which is used to trigger auto generation of security salt/key.
357 */
358 public function get_is_autogenerate_keys(): bool {
359 $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' );
360
361 $this->is_autogenerate_keys = is_null( $is_autogenerate_keys )
362 ? true
363 : (bool) $is_autogenerate_keys;
364
365 return $this->is_autogenerate_keys;
366 }
367
368 /**
369 * Setter method of is_autogenerate_keys.
370 *
371 * @param bool $value Boolean flag of the is_autogenerate_keys.
372 *
373 * @return bool Return true if value updated, otherwise false.
374 */
375 public function set_is_autogenrate_keys( bool $value ): bool {
376 $this->is_autogenerate_keys = (bool) $value;
377
378 return $this->update_option(
379 'is_autogenerate_keys',
380 $this->is_autogenerate_keys
381 );
382 }
383
384 /**
385 * Method to initialize component hooks.
386 *
387 * @return void
388 */
389 public function add_hooks(): void {
390 add_action( 'wpdef_sec_key_gen', array( $this, 'cron_process' ) );
391 }
392
393 /**
394 * Get cron schedule details.
395 *
396 * @return array
397 */
398 private function get_cron_schedule_details(): array {
399 $display_name = $this->get_option( 'reminder_duration' );
400
401 if ( empty( $display_name ) ) {
402 $display_name = $this->default_days;
403 }
404 $schedule_detail = $this->get_schedule_detail( $display_name );
405
406 $schedule_key = key( $schedule_detail );
407 $schedule_interval = time();
408
409 if ( null !== $schedule_key && isset( $schedule_detail[ $schedule_key ]['interval'] ) ) {
410 $schedule_interval = $schedule_interval + $schedule_detail[ $schedule_key ]['interval'];
411 }
412
413 return array(
414 'key' => $schedule_key,
415 'interval' => $schedule_interval,
416 );
417 }
418
419 /**
420 * Cron schedule.
421 *
422 * @return void
423 */
424 public function cron_schedule(): void {
425 if (
426 true === $this->get_is_autogenerate_keys() &&
427 ! wp_next_scheduled( 'wpdef_sec_key_gen' )
428 ) {
429 $schedule_details = $this->get_cron_schedule_details();
430
431 wp_schedule_event(
432 $schedule_details['interval'],
433 $schedule_details['key'],
434 'wpdef_sec_key_gen'
435 );
436 }
437 }
438
439 /**
440 * Cron unscheduled.
441 *
442 * @return void
443 */
444 public function cron_unschedule(): void {
445 $timestamp = wp_next_scheduled( 'wpdef_sec_key_gen' );
446
447 if ( $timestamp ) {
448 wp_unschedule_event( $timestamp, 'wpdef_sec_key_gen' );
449 }
450 }
451
452 /**
453 * Cron schedule security key/salt generation process.
454 *
455 * @return void
456 */
457 public function cron_process(): void {
458 try {
459 if ( is_multisite() ) {
460 $next_run = get_site_option( self::REGENERATE_SALT_NEXT_RUN_OPTION, 0 );
461 if ( ! empty( $next_run ) && $next_run > time() ) {
462 return;
463 }
464
465 $schedule_details = $this->get_cron_schedule_details();
466 update_site_option( self::REGENERATE_SALT_NEXT_RUN_OPTION, $schedule_details['interval'] );
467 }
468
469 $is_switch_to_main_site = is_multisite() && ! is_main_site();
470 if ( $is_switch_to_main_site ) {
471 switch_to_blog( get_main_site_id() );
472 }
473
474 $security_tweak_model = new Security_Tweaks();
475 if ( ! $security_tweak_model->is_tweak_ignore( $this->slug ) &&
476 true === $this->get_is_autogenerate_keys()
477 ) {
478 $this->process();
479 }
480
481 if ( $is_switch_to_main_site ) {
482 restore_current_blog();
483 }
484 } catch ( Throwable $th ) {
485 // PHP 7+ catch.
486 $this->log( 'Security Key. Cron Throwable: ' . get_class( $th ) . ': ' . $th->getMessage(), Security_Tweak::LOG_FILE_NAME );
487 } catch ( Exception $e ) {
488 $this->log( 'Security Key. Cron Exception: ' . get_class( $e ) . ': ' . $e->getMessage(), Security_Tweak::LOG_FILE_NAME );
489 }
490 }
491
492 /**
493 * Get scheduler detail.
494 *
495 * @param string $display_name The name to filter the schedules list.
496 *
497 * @return array Return filtered schedule detail.
498 */
499 private function get_schedule_detail( $display_name ) {
500 $all_schedules = wp_get_schedules();
501
502 $schedule_detail = array_filter(
503 $all_schedules,
504 function ( $arr ) use ( $display_name ) {
505 return $display_name === $arr['display'];
506 }
507 );
508
509 return $schedule_detail;
510 }
511
512 /**
513 * Check all salts are exists.
514 *
515 * @return bool Return true if all constants have salt else false.
516 */
517 private function is_salts_exist(): bool {
518 foreach ( $this->get_constants() as $constants ) {
519 if ( ! defined( $constants ) ) {
520 return false;
521 }
522 }
523
524 return true;
525 }
526
527 /**
528 * Append salt to wp-config.php file content string.
529 *
530 * @param string|false $contents Text content of wp-config.php.
531 * @param string $new_salts Salts need to be appended.
532 *
533 * @return string Text content after salts appended.
534 */
535 private function append_salts( $contents, string $new_salts ): string {
536 $pattern = '/(define\(\s*\'AUTH_KEY\'.*?\);)\s*' .
537 '(define\(\s*\'SECURE_AUTH_KEY\'.*?\);)\s*' .
538 '(define\(\s*\'LOGGED_IN_KEY\'.*?\);)\s*' .
539 '(define\(\s*\'NONCE_KEY\'.*?\);)\s*' .
540 '(define\(\s*\'AUTH_SALT\'.*?\);)\s*' .
541 '(define\(\s*\'SECURE_AUTH_SALT\'.*?\);)\s*' .
542 '(define\(\s*\'LOGGED_IN_SALT\'.*?\);)\s*' .
543 '(define\(\s*\'NONCE_SALT\'.*?\);)/s';
544
545 if ( preg_match( $pattern, $contents ) ) {
546 return preg_replace( $pattern, $new_salts, $contents );
547 } else {
548 return preg_replace( '/(define\(\s*\'DB_NAME\'\s*,.*?\);)/s', $new_salts . PHP_EOL . '$1', $contents );
549 }
550 }
551
552 /**
553 * Return collection of reminder frequencies.
554 *
555 * @return array
556 */
557 public function reminder_frequencies(): array {
558 return array(
559 '30 days',
560 '60 days',
561 '90 days',
562 '6 months',
563 '1 year',
564 );
565 }
566
567 /**
568 * Get the last modified time of wp-config file.
569 *
570 * @return int|false
571 * @since 3.10.0
572 */
573 private function get_wp_config_last_modified_time() {
574 $file = $this->file;
575 if ( ! file_exists( $file ) ) {
576 $file = ABSPATH . WPINC . '/general-template.php';
577 }
578
579 return filemtime( $file );
580 }
581 }
582