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

542 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 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 * 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 > defender_get_current_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 = $options['reminder_date'] ?? 0;
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 }
120
121 /**
122 * Here is the code for processing. If the return is true or Response, we add it to resolve list. WP_Error if any error.
123 *
124 * @return bool|WP_Error|Response
125 */
126 public function process() {
127 $is_done = $this->update_keys();
128
129 if ( is_wp_error( $is_done ) ) {
130 return $is_done;
131 }
132
133 if ( $is_done ) {
134 $mask_login = wd_di()->get( Mask_Login::class );
135 $url = $mask_login->is_active()
136 ? $mask_login->get_new_login_url()
137 : wp_login_url( network_admin_url( 'admin.php?page=wdf-hardener' ) );
138
139 $interval = 3;
140
141 return new Response(
142 true,
143 array(
144 'message' => sprintf(
145 /* translators: 1: login link, 2: line break, 3: timer. */
146 esc_html__(
147 'All key salts have been regenerated. You will now need to %1$s. %2$s This will auto reload after %3$s seconds.',
148 'defender-security'
149 ),
150 '<a href="' . $url . '"><strong>' . esc_html__( 're-login', 'defender-security' ) . '</strong></a>',
151 '<br>',
152 '<span class="hardener-timer">' . $interval . '</span>'
153 ),
154 'redirect' => $url,
155 'interval' => $interval,
156 )
157 );
158 }
159
160 return $is_done;
161 }
162
163 /**
164 * This is for un-do stuff that has be done in @process.
165 *
166 * @return bool
167 */
168 public function revert(): bool {
169 return true;
170 }
171
172 /**
173 * Shield up.
174 *
175 * @return bool
176 */
177 public function shield_up(): bool {
178 return true;
179 }
180
181 /**
182 * Get salts to be placed in wp-config.php.
183 *
184 * @return array|WP_Error
185 */
186 private function get_salts(): WP_Error|array {
187 $response = wp_safe_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' );
188
189 if ( is_wp_error( $response ) ) {
190 return new WP_Error(
191 'defender_salts_not_found',
192 esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' )
193 );
194 }
195
196 $raw_salts = explode( "\n", wp_remote_retrieve_body( $response ) );
197 $salts = array();
198
199 foreach ( $raw_salts as $salt ) {
200 $salt = trim( $salt );
201 if ( '' === $salt ) {
202 continue;
203 }
204 $salts[] = stripslashes( $salt );
205 }
206
207 return $salts;
208 }
209
210 /**
211 * Get how long the wp-config file is last updated.
212 *
213 * @return int|string
214 */
215 private function get_last_modified_days(): int|string {
216 $current_time = defender_get_current_time();
217 $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS;
218 return $days_ago ? (int) round( $days_ago ) : 'unknown';
219 }
220
221 /**
222 * Get all the constants.
223 *
224 * @return array
225 */
226 private function get_constants(): array {
227 return array(
228 'AUTH_KEY',
229 'SECURE_AUTH_KEY',
230 'LOGGED_IN_KEY',
231 'NONCE_KEY',
232 'AUTH_SALT',
233 'SECURE_AUTH_SALT',
234 'LOGGED_IN_SALT',
235 'NONCE_SALT',
236 );
237 }
238
239 /**
240 * Retrieve the tweak's label.
241 *
242 * @return string
243 */
244 public function get_label(): string {
245 return esc_html__( 'Update old security keys', 'defender-security' );
246 }
247
248 /**
249 * Get the error reason.
250 *
251 * @return string
252 */
253 public function get_error_reason(): string {
254 $get_last_modified_days = $this->get_last_modified_days();
255
256 if ( 'unknown' === $get_last_modified_days ) {
257 $error_message = esc_html__(
258 'We can\'t tell how old your security keys are, perhaps it\'s time to update them?',
259 'defender-security'
260 );
261 }
262 if ( ! $this->is_salts_exist() ) {
263 $error_message = esc_html__(
264 'One or more security salts aren\'t defined in wp-config.php. Time to regenerate them!',
265 'defender-security'
266 );
267 } else {
268 $error_message = sprintf(
269 /* translators: %s: number of days */
270 esc_html__( 'Your current security keys are %s days old. Time to update them!', 'defender-security' ),
271 $get_last_modified_days
272 );
273 }
274
275 return $error_message;
276 }
277
278 /**
279 * Return a summary data of this tweak.
280 *
281 * @return array
282 */
283 public function to_array(): array {
284 return array(
285 'slug' => $this->slug,
286 'title' => $this->get_label(),
287 'errorReason' => $this->get_error_reason(),
288 'successReason' => sprintf(
289 /* translators: %s: number of days */
290 esc_html__( 'Your security keys are less than %s days old, nice work.', 'defender-security' ),
291 $this->get_last_modified_days()
292 ),
293 'misc' => array(
294 'reminder' => $this->reminder_duration,
295 ),
296 'bulk_description' => esc_html__(
297 'Your current security keys are unknown days old. Time to update them! We will update the frequency to 60 days.',
298 'defender-security'
299 ),
300 'bulk_title' => esc_html__( 'Security Keys', 'defender-security' ),
301 );
302 }
303
304 /**
305 * Getter method of is_autogenerate_keys.
306 *
307 * @return bool. Return true or false which is used to trigger auto generation of security salt/key.
308 */
309 public function get_is_autogenerate_keys(): bool {
310 $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' );
311
312 $this->is_autogenerate_keys = is_null( $is_autogenerate_keys ) || (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 = $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 = defender_get_current_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 bool|string $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( bool|string $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(): bool|int {
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 $wp_filesystem = $this->get_wp_filesystem();
478
479 if ( ! $wp_filesystem->is_writable( $this->file ) ) {
480 return new WP_Error(
481 'defender_file_not_writable',
482 /* translators: %s: file path */
483 sprintf( esc_html__( 'The file %s is not writable', 'defender-security' ), $this->file )
484 );
485 }
486
487 $constants = $this->get_constants();
488 $salts = $this->get_salts();
489
490 if ( is_wp_error( $salts ) ) {
491 return $salts;
492 }
493
494 $contents = $wp_filesystem->get_contents( $this->file );
495
496 $new_salts = '';
497
498 foreach ( $constants as $key => $const ) {
499 if ( defined( $const ) ) {
500 $pattern = "/^define\(\s*['|\"]{$const}['|\"],(.*)\)\s*;/m";
501 $replacement = $salts[ $key ];
502 $contents = preg_replace_callback(
503 $pattern,
504 function () use ( $replacement ) {
505 return $replacement;
506 },
507 $contents
508 );
509 } else {
510 $new_salts .= $salts[ $key ] . PHP_EOL;
511 }
512 }
513
514 if ( '' !== $new_salts ) {
515 $new_salts = PHP_EOL .
516 '/* DEFENDER GENERATED SALTS */' .
517 PHP_EOL .
518 $new_salts .
519 PHP_EOL;
520
521 $contents = $this->append_salts( $contents, $new_salts );
522 }
523
524 $is_done = $wp_filesystem->put_contents( $this->file, $contents, FS_CHMOD_FILE );
525
526 if ( ! $is_done ) {
527 return false;
528 }
529 $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
530 $this->last_modified = defender_get_current_time();
531 $values['last_modified'] = defender_get_current_time();
532 update_site_option( 'defender_security_tweaks_' . $this->slug, $values );
533 $this->log( 'Security keys are updated.', Security_Tweak::LOG_FILE_NAME );
534 if ( is_multisite() && ! headers_sent() ) {
535 // Delete cookies forced.
536 wp_clear_auth_cookie();
537 }
538
539 return true;
540 }
541 }
542