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

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