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

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