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

542 lines 13.2 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 = ! 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 $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( explode( "\n", wp_remote_retrieve_body( $response ) ) );
199 }
200
201 /**
202 * Get how long the wp-config file is last updated.
203 *
204 * @return int|string
205 */
206 private function get_last_modified_days() {
207 $current_time = time();
208 $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS;
209 $days_ago = $days_ago ? (int) round( $days_ago ) : 'unknown';
210
211 return $days_ago ?? 1;
212 }
213
214 /**
215 * Get all the constants.
216 *
217 * @return array
218 */
219 private function get_constants(): array {
220 return array(
221 'AUTH_KEY',
222 'SECURE_AUTH_KEY',
223 'LOGGED_IN_KEY',
224 'NONCE_KEY',
225 'AUTH_SALT',
226 'SECURE_AUTH_SALT',
227 'LOGGED_IN_SALT',
228 'NONCE_SALT',
229 );
230 }
231
232 /**
233 * Retrieve the tweak's label.
234 *
235 * @return string
236 */
237 public function get_label(): string {
238 return esc_html__( 'Update old security keys', 'defender-security' );
239 }
240
241 /**
242 * Get the error reason.
243 *
244 * @return string
245 */
246 public function get_error_reason(): string {
247 $get_last_modified_days = $this->get_last_modified_days();
248
249 if ( 'unknown' === $get_last_modified_days ) {
250 $error_message = esc_html__(
251 'We can\'t tell how old your security keys are, perhaps it\'s time to update them?',
252 'defender-security'
253 );
254 }
255 if ( ! $this->is_salts_exist() ) {
256 $error_message = esc_html__(
257 'One or more security salts aren\'t defined in wp-config.php. Time to regenerate them!',
258 'defender-security'
259 );
260 } else {
261 $error_message = sprintf(
262 /* translators: %s: number of days */
263 esc_html__( 'Your current security keys are %s days old. Time to update them!', 'defender-security' ),
264 $get_last_modified_days
265 );
266 }
267
268 return $error_message;
269 }
270
271 /**
272 * Return a summary data of this tweak.
273 *
274 * @return array
275 */
276 public function to_array(): array {
277 return array(
278 'slug' => $this->slug,
279 'title' => $this->get_label(),
280 'errorReason' => $this->get_error_reason(),
281 'successReason' => sprintf(
282 /* translators: %s: number of days */
283 esc_html__( 'Your security keys are less than %s days old, nice work.', 'defender-security' ),
284 $this->get_last_modified_days()
285 ),
286 'misc' => array(
287 'reminder' => $this->reminder_duration,
288 ),
289 'bulk_description' => esc_html__(
290 'Your current security keys are unknown days old. Time to update them! We will update the frequency to 60 days.',
291 'defender-security'
292 ),
293 'bulk_title' => esc_html__( 'Security Keys', 'defender-security' ),
294 );
295 }
296
297 /**
298 * Getter method of is_autogenerate_keys.
299 *
300 * @return bool. Return true or false which is used to trigger auto generation of security salt/key.
301 */
302 public function get_is_autogenerate_keys(): bool {
303 $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' );
304
305 $this->is_autogenerate_keys = is_null( $is_autogenerate_keys )
306 ? true
307 : (bool) $is_autogenerate_keys;
308
309 return $this->is_autogenerate_keys;
310 }
311
312 /**
313 * Setter method of is_autogenerate_keys.
314 *
315 * @param bool $value Boolean flag of the is_autogenerate_keys.
316 *
317 * @return bool Return true if value updated, otherwise false.
318 */
319 public function set_is_autogenrate_keys( bool $value ): bool {
320 $this->is_autogenerate_keys = (bool) $value;
321
322 return $this->update_option(
323 'is_autogenerate_keys',
324 $this->is_autogenerate_keys
325 );
326 }
327
328 /**
329 * Cron schedule.
330 *
331 * @return void
332 */
333 public function cron_schedule(): void {
334 if (
335 true === $this->get_is_autogenerate_keys()
336 ) {
337 $display_name = $this->get_option( 'reminder_duration' );
338
339 if ( empty( $display_name ) ) {
340 $display_name = $this->default_days;
341 }
342
343 /**
344 * Network Cron Manager instance.
345 *
346 * @var Network_Cron_Manager $network_cron_manager
347 */
348 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
349
350 $interval_seconds = strtotime( $display_name, 0 );
351 $start_time = time() + $interval_seconds;
352
353 $network_cron_manager->register_callback(
354 'wpdef_sec_key_gen',
355 array( $this, 'cron_process' ),
356 $interval_seconds,
357 $start_time,
358 );
359
360 }
361 }
362
363 /**
364 * Cron unscheduled.
365 *
366 * @return void
367 */
368 public function cron_unschedule(): void {
369 /**
370 * Network Cron Manager instance.
371 *
372 * @var Network_Cron_Manager $network_cron_manager
373 */
374 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
375 $network_cron_manager->remove_callback( 'wpdef_sec_key_gen' );
376 }
377
378 /**
379 * Cron schedule security key/salt generation process.
380 *
381 * @return void
382 */
383 public function cron_process(): void {
384 try {
385 $security_tweak_model = new Security_Tweaks();
386 if ( ! $security_tweak_model->is_tweak_ignore( $this->slug ) &&
387 true === $this->get_is_autogenerate_keys()
388 ) {
389 $this->update_keys();
390 }
391 } catch ( Throwable $th ) {
392 $this->log( 'Security Key. Cron Throwable: ' . get_class( $th ) . ': ' . $th->getMessage(), Security_Tweak::LOG_FILE_NAME );
393 }
394 }
395
396 /**
397 * Check all salts are exists.
398 *
399 * @return bool Return true if all constants have salt else false.
400 */
401 private function is_salts_exist(): bool {
402 foreach ( $this->get_constants() as $constants ) {
403 if ( ! defined( $constants ) ) {
404 return false;
405 }
406 }
407
408 return true;
409 }
410
411 /**
412 * Append salt to wp-config.php file content string.
413 *
414 * @param string|false $contents Text content of wp-config.php.
415 * @param string $new_salts Salts need to be appended.
416 *
417 * @return string Text content after salts appended.
418 */
419 private function append_salts( $contents, string $new_salts ): string {
420 $pattern = '/(define\(\s*\'AUTH_KEY\'.*?\);)\s*' .
421 '(define\(\s*\'SECURE_AUTH_KEY\'.*?\);)\s*' .
422 '(define\(\s*\'LOGGED_IN_KEY\'.*?\);)\s*' .
423 '(define\(\s*\'NONCE_KEY\'.*?\);)\s*' .
424 '(define\(\s*\'AUTH_SALT\'.*?\);)\s*' .
425 '(define\(\s*\'SECURE_AUTH_SALT\'.*?\);)\s*' .
426 '(define\(\s*\'LOGGED_IN_SALT\'.*?\);)\s*' .
427 '(define\(\s*\'NONCE_SALT\'.*?\);)/s';
428
429 if ( preg_match( $pattern, $contents ) ) {
430 return preg_replace( $pattern, $new_salts, $contents );
431 } else {
432 return preg_replace( '/(define\(\s*\'DB_NAME\'\s*,.*?\);)/s', $new_salts . PHP_EOL . '$1', $contents );
433 }
434 }
435
436 /**
437 * Return collection of reminder frequencies.
438 *
439 * @return array
440 */
441 public function reminder_frequencies(): array {
442 return array(
443 '30 days',
444 '60 days',
445 '90 days',
446 '6 months',
447 '1 year',
448 );
449 }
450
451 /**
452 * Get the last modified time of wp-config file.
453 *
454 * @return int|false
455 * @since 3.10.0
456 */
457 private function get_wp_config_last_modified_time() {
458 $file = $this->file;
459 if ( ! file_exists( $file ) ) {
460 $file = ABSPATH . WPINC . '/general-template.php';
461 }
462
463 return filemtime( $file );
464 }
465
466 /**
467 * Update keys.
468 *
469 * @return WP_Error|bool|array True if success. WP_Error if any error.
470 */
471 private function update_keys(): WP_Error|bool|array {
472 global $wp_filesystem;
473
474 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
475 require_once ABSPATH . '/wp-admin/includes/file.php';
476 WP_Filesystem();
477 }
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 ( ! empty( $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 = (bool) file_put_contents( $this->file, $contents, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
525
526 if ( ! $is_done ) {
527 return false;
528 }
529 $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() );
530 $this->last_modified = time();
531 $values['last_modified'] = 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