PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.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 / servers / class-apache.php

class-apache.php in Defender Security – Malware Scanner, Login Security & Firewall 5.5.1, at src/component/security-tweaks/servers/class-apache.php

647 lines 18.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for managing server configurations related to security tweaks .
4 *
5 * @package WP_Defender\Component\Security_Tweaks\Servers
6 */
7
8 namespace WP_Defender\Component\Security_Tweaks\Servers;
9
10 use WP_Error;
11
12 /**
13 * Provides methods to apply and revert security rules on servers.
14 */
15 class Apache {
16
17 public const CACHE_APACHE_VERSION = 'defender_apache_version';
18
19 /**
20 * Exclude file paths.
21 *
22 * @var array
23 */
24 public $new_htaccess_config = array();
25
26 /**
27 * The htaccess inside wp-content.
28 *
29 * @var string
30 */
31 public $contentdir_path = null;
32
33 /**
34 * The htaccess path inside wp-includes.
35 *
36 * @var null
37 */
38 public $includedir_path = null;
39
40 /**
41 * Service type.
42 *
43 * @var string
44 */
45 private $type = null;
46
47 /**
48 * Constructor for class.
49 *
50 * @param string $type The type of the security tweak.
51 */
52 public function __construct( $type ) {
53 $this->type = $type;
54 }
55
56 /**
57 * Check whether the issue has been resolved or not.
58 *
59 * @return bool
60 */
61 public function check() {
62 $url = '';
63
64 if ( 'prevent-php-executed' === $this->type ) {
65 $dir = wp_upload_dir();
66 $url = $dir['baseurl'] . '/wp-defender/index.php';
67 }
68
69 if ( 'protect-information' === $this->type ) {
70 $url = defender_asset_url( '/languages/' . WP_DEFENDER_POT_FILENAME );
71 }
72
73 return Server::ping_test_failed( $url );
74 }
75
76 /**
77 * Retrieves a WP_Error object based on the provided file path.
78 *
79 * @param string $file_path The path to the file.
80 *
81 * @return WP_Error Returns a WP_Error indicating the file is not writable.
82 */
83 public function get_error_by_file_path( $file_path ) {
84 return new WP_Error(
85 'defender_file_not_writable',
86 sprintf(
87 /* translators: %s - file path */
88 esc_html__( 'The file %s is not writable', 'defender-security' ),
89 $file_path
90 )
91 );
92 }
93
94 /**
95 * Process the rule.
96 *
97 * @return bool|WP_Error
98 */
99 public function process() {
100 global $wp_filesystem;
101 // Initialize the WP filesystem, no more using 'file-put-contents' function.
102 if ( empty( $wp_filesystem ) ) {
103 require_once ABSPATH . '/wp-admin/includes/file.php';
104 WP_Filesystem();
105 }
106 if ( 'protect-information' === $this->type ) {
107 $ht_access_path = ABSPATH . '.htaccess';
108 // Create a htaccess-file if it doesn't exist.
109 if ( ! is_file( $ht_access_path )
110 && false === (bool) file_put_contents( $ht_access_path, '', LOCK_EX ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
111 ) {
112 return $this->get_error_by_file_path( $ht_access_path );
113 }
114 // If the htaccess-file is existed.
115 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
116 return $this->get_error_by_file_path( $ht_access_path );
117 }
118
119 $ht_access_config = file( $ht_access_path );
120 $ht_access_config = array_map( 'trim', $ht_access_config );
121 $rules = $this->get_rules();
122 $contains_search = array_diff( array_map( 'trim', $rules ), $ht_access_config );
123
124 if ( count( $contains_search ) < count( $rules ) ) {
125 // Search the wrapper block.
126 $ht_access_content = $wp_filesystem->get_contents( $ht_access_path );
127 preg_match( '/## WP Defender(.*?)## WP Defender - End ##/s', $ht_access_content, $matches );
128
129 if ( count( $matches ) ) {
130 // Remove the whole parts as its partial done.
131 $ht_access_content = str_replace( $matches[0], '', $ht_access_content );
132 $ht_access_config = explode( PHP_EOL, $ht_access_content );
133 $ht_access_config = array_merge( $ht_access_config, $rules );
134
135 return (bool) file_put_contents( $ht_access_path, implode( PHP_EOL, $ht_access_config ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
136 }
137 }
138
139 if ( 0 === count( $contains_search ) || ( count( $contains_search ) === count( $rules ) ) ) {
140 $ht_access_config = array_merge( $ht_access_config, $rules );
141
142 return (bool) file_put_contents( $ht_access_path, implode( PHP_EOL, $ht_access_config ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
143 }
144 }
145
146 if ( 'prevent-php-executed' === $this->type ) {
147 if ( ! in_array( Server::get_current_server(), array( 'apache', 'litespeed' ), true ) ) {
148 return new WP_Error(
149 'defender_unable_to_apply_rules',
150 esc_html__(
151 'The rules can\'t be applied. This can be either because your host doesn\'t allow editing the file, or you\'ve selected the wrong server type.',
152 'defender-security'
153 )
154 );
155 }
156
157 $response = $this->protect_content_directory();
158
159 if ( is_wp_error( $response ) ) {
160 return $response;
161 }
162
163 $response = $this->protect_includes_directory();
164
165 if ( is_wp_error( $response ) ) {
166 return $response;
167 }
168
169 $response = $this->protect_uploads_directory();
170
171 if ( is_wp_error( $response ) ) {
172 return $response;
173 }
174
175 $settings = array(
176 'new_htaccess_config' => $this->get_new_htaccess_config(),
177 );
178
179 update_site_option( "defender_security_tweeks_{$this->type}", $settings );
180
181 return true;
182 }
183 }
184
185 /**
186 * Revert the rules.
187 *
188 * @return bool|WP_Error
189 */
190 public function revert() {
191 global $wp_filesystem;
192 // Initialize the WP filesystem, no more using 'file-put-contents' function.
193 if ( empty( $wp_filesystem ) ) {
194 require_once ABSPATH . '/wp-admin/includes/file.php';
195 WP_Filesystem();
196 }
197 if ( 'protect-information' === $this->type ) {
198 $ht_access_path = ABSPATH . '.htaccess';
199 // Quick exit if the file does not exist.
200 if ( ! is_file( $ht_access_path ) ) {
201 return true;
202 }
203
204 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
205 return $this->get_error_by_file_path( $ht_access_path );
206 }
207
208 $ht_access_config = $wp_filesystem->get_contents( $ht_access_path );
209 $rules = $this->get_rules();
210
211 preg_match_all( '/## WP Defender(.*?)## WP Defender - End ##/s', $ht_access_config, $matches );
212
213 if ( is_array( $matches ) && count( $matches ) > 0 ) {
214 $ht_access_config = str_replace( implode( '', $matches[0] ), '', $ht_access_config );
215 } else {
216 $ht_access_config = str_replace( implode( '', $rules ), '', $ht_access_config );
217 }
218
219 $ht_access_config = trim( $ht_access_config );
220
221 return (bool) file_put_contents( $ht_access_path, $ht_access_config, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
222 }
223
224 if ( 'prevent-php-executed' === $this->type ) {
225 // Content-folder.
226 $response = $this->unprotect_content_directory();
227 if ( is_wp_error( $response ) ) {
228 return wp_send_json_error(
229 array( 'message' => $response->get_error_message() )
230 );
231 }
232 // Includes-folder.
233 $response = $this->unprotect_includes_directory();
234 if ( is_wp_error( $response ) ) {
235 return wp_send_json_error(
236 array( 'message' => $response->get_error_message() )
237 );
238 }
239 // Uploads-folder.
240 $response = $this->unprotect_upload_directory();
241 if ( is_wp_error( $response ) ) {
242 return wp_send_json_error(
243 array( 'message' => $response->get_error_message() )
244 );
245 }
246
247 return delete_site_option( "defender_security_tweeks_{$this->type}" );
248 }
249 }
250
251 /**
252 * Get Apache rule depending on the version.
253 *
254 * @return array
255 */
256 public function get_rules() {
257 $rules = array(
258 PHP_EOL . '## WP Defender - Prevent information disclosure ##' . PHP_EOL,
259 '<FilesMatch "\.(md|exe|sh|bak|inc|pot|po|mo|log|sql)$">' . PHP_EOL .
260 'Require all denied' . PHP_EOL .
261 '</FilesMatch>' . PHP_EOL,
262 '<Files robots.txt>' . PHP_EOL .
263 'Require all granted' . PHP_EOL .
264 '</Files>' . PHP_EOL,
265 '<Files ads.txt>' . PHP_EOL .
266 'Require all granted' . PHP_EOL .
267 '</Files>' . PHP_EOL,
268 '## WP Defender - End ##',
269 );
270
271 if ( version_compare( $this->get_version(), '2.4', '<' ) ) {
272 $rules = array(
273 PHP_EOL . '## WP Defender - Prevent information disclosure ##' . PHP_EOL,
274 '<FilesMatch "\.(md|exe|sh|bak|inc|pot|po|mo|log|sql)$">' . PHP_EOL .
275 'Order allow,deny' . PHP_EOL .
276 'Deny from all' . PHP_EOL .
277 '</FilesMatch>' . PHP_EOL,
278 '<Files robots.txt>' . PHP_EOL .
279 'Allow from all' . PHP_EOL .
280 '</Files>' . PHP_EOL,
281 '<Files ads.txt>' . PHP_EOL .
282 'Allow from all' . PHP_EOL .
283 '</Files>' . PHP_EOL,
284 '## WP Defender - End ##',
285 );
286 }
287
288 return $rules;
289 }
290
291 /**
292 * Get Apache rule depending on the version for instruction on browser.
293 *
294 * @return string
295 */
296 public function get_rules_for_instruction() {
297 $rules = '';
298
299 if ( 'prevent-php-executed' === $this->type ) {
300 $rules = '## WP Defender - Protect PHP Executed ##' . PHP_EOL;
301 $rules .= PHP_EOL;
302 $rules .= '<Files *.php>' . PHP_EOL;
303 $rules .= 'Require all denied' . PHP_EOL;
304 $rules .= '</Files>' . PHP_EOL;
305 $rules .= PHP_EOL;
306 $rules .= '## WP Defender - End ##' . PHP_EOL;
307
308 if ( version_compare( $this->get_version(), '2.4', '<' ) ) {
309 $rules = '## WP Defender - Protect PHP Executed ##' . PHP_EOL;
310 $rules .= PHP_EOL;
311 $rules .= '<Files *.php>' . PHP_EOL;
312 $rules .= 'Order allow,deny' . PHP_EOL;
313 $rules .= 'Deny from all' . PHP_EOL;
314 $rules .= '</Files>' . PHP_EOL;
315 $rules .= PHP_EOL;
316 $rules .= '## WP Defender - End ##' . PHP_EOL;
317 }
318 }
319
320 if ( 'protect-information' === $this->type ) {
321 $rules = '## WP Defender - Prevent information disclosure ##' . PHP_EOL;
322 $rules .= PHP_EOL;
323 $rules .= '<FilesMatch "\.(md|exe|sh|bak|inc|pot|po|mo|log|sql)$">' . PHP_EOL;
324 $rules .= 'Require all denied' . PHP_EOL;
325 $rules .= '</FilesMatch>' . PHP_EOL;
326 $rules .= PHP_EOL;
327 $rules .= '<Files robots.txt>' . PHP_EOL;
328 $rules .= 'Require all granted' . PHP_EOL;
329 $rules .= '</Files>' . PHP_EOL;
330 $rules .= PHP_EOL;
331 $rules .= '<Files ads.txt>' . PHP_EOL;
332 $rules .= 'Require all granted' . PHP_EOL;
333 $rules .= '</Files>' . PHP_EOL;
334 $rules .= PHP_EOL;
335 $rules .= '## WP Defender - End ##';
336
337 if ( version_compare( $this->get_version(), '2.4', '<' ) ) {
338 $rules = '## WP Defender - Prevent information disclosure ##' . PHP_EOL;
339 $rules .= PHP_EOL;
340 $rules .= '<FilesMatch "\.(md|exe|sh|bak|inc|pot|po|mo|log|sql)$">' . PHP_EOL;
341 $rules .= 'Order allow,deny' . PHP_EOL;
342 $rules .= 'Deny from all' . PHP_EOL;
343 $rules .= '</FilesMatch>' . PHP_EOL;
344 $rules .= PHP_EOL;
345 $rules .= '<Files robots.txt>' . PHP_EOL;
346 $rules .= 'Allow from all' . PHP_EOL;
347 $rules .= '</Files>' . PHP_EOL;
348 $rules .= PHP_EOL;
349 $rules .= '<Files ads.txt>' . PHP_EOL;
350 $rules .= 'Allow from all' . PHP_EOL;
351 $rules .= '</Files>' . PHP_EOL;
352 $rules .= PHP_EOL;
353 $rules .= '## WP Defender - End ##';
354 }
355 }
356
357 return $rules;
358 }
359
360 /**
361 * Determine the Apache version.
362 * Most web servers have apache_get_version disabled, so we just get a simple curl of the headers.
363 *
364 * @return string
365 */
366 public function get_version() {
367 if ( ! function_exists( 'apache_get_version' ) ) {
368 // The default support is 2.2.
369 $version = '2.2';
370 $url = home_url();
371 $apache_version = get_site_transient( self::CACHE_APACHE_VERSION );
372
373 if ( ! is_array( $apache_version ) ) {
374 $apache_version = array();
375 }
376
377 if ( isset( $apache_version[ $url ] ) && ! empty( $apache_version[ $url ] ) ) {
378 return strtolower( $apache_version[ $url ] );
379 }
380 // The default support is 2.2.
381 $apache_version[ $url ] = $version;
382
383 $server = defender_get_data_from_request( 'SERVER_SOFTWARE', 's' );
384 if ( isset( $server ) ) {
385 $server = explode( ' ', $server );
386 if ( is_array( $server ) && count( $server ) > 1 ) {
387 $server = $server[0];
388 $server = explode( '/', $server );
389 if ( is_array( $server ) && count( $server ) > 1 ) {
390 $version = $server[1];
391 $apache_version[ $url ] = $version;
392 }
393 }
394 }
395
396 set_site_transient( self::CACHE_APACHE_VERSION, $apache_version, 3600 );
397 } else {
398 $version = apache_get_version();
399 $version = explode( '/', $version );
400 $version = ! empty( $version[1] ) ? $version[1] : $version[0];
401 }
402
403 return $version;
404 }
405
406 /**
407 * Protect content directory.
408 *
409 * @return void|WP_Error
410 */
411 public function protect_content_directory() {
412 global $wp_filesystem;
413 // Initialize the WP filesystem, no more using 'file-put-contents' function.
414 if ( empty( $wp_filesystem ) ) {
415 require_once ABSPATH . '/wp-admin/includes/file.php';
416 WP_Filesystem();
417 }
418 $ht_access_path = $this->contentdir_path;
419
420 if ( empty( $ht_access_path ) ) {
421 $ht_access_path = WP_CONTENT_DIR . '/.htaccess';
422 }
423 // Create a htaccess-file if it doesn't exist.
424 if ( ! is_file( $ht_access_path )
425 && false === (bool) file_put_contents( $ht_access_path, '', LOCK_EX ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
426 ) {
427 return $this->get_error_by_file_path( $ht_access_path );
428 }
429 // If the htaccess-file is existed.
430 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
431 return $this->get_error_by_file_path( $ht_access_path );
432 }
433
434 $exists_rules = $this->cleanup_old_rules( $wp_filesystem->get_contents( $ht_access_path ) );
435 $rule = array(
436 '## WP Defender - Protect PHP Executed ##',
437 '<Files *.php>',
438 $this->generate_htaccess_rule( false ),
439 '</Files>',
440 );
441
442 $rule[] = '## WP Defender - End ##';
443 file_put_contents( $ht_access_path, $exists_rules . implode( PHP_EOL, $rule ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
444 }
445
446 /**
447 * Protect includes directory.
448 *
449 * @return void|WP_Error
450 */
451 public function protect_includes_directory() {
452 global $wp_filesystem;
453 // Initialize the WP filesystem, no more using 'file-put-contents' function.
454 if ( empty( $wp_filesystem ) ) {
455 require_once ABSPATH . '/wp-admin/includes/file.php';
456 WP_Filesystem();
457 }
458 $ht_access_path = $this->includedir_path;
459
460 if ( empty( $ht_access_path ) ) {
461 $ht_access_path = ABSPATH . WPINC . '/.htaccess';
462 }
463 // Create a htaccess-file if it doesn't exist.
464 if ( ! is_file( $ht_access_path )
465 && false === (bool) file_put_contents( $ht_access_path, '', LOCK_EX ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
466 ) {
467 return $this->get_error_by_file_path( $ht_access_path );
468 }
469 // If the htaccess-file is existed.
470 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
471 return $this->get_error_by_file_path( $ht_access_path );
472 }
473
474 $exists_rules = $this->cleanup_old_rules( $wp_filesystem->get_contents( $ht_access_path ) );
475
476 $rule = array(
477 '## WP Defender - Protect PHP Executed ##',
478 '<Files *.php>',
479 $this->generate_htaccess_rule( false ),
480 '</Files>',
481 '<Files wp-tinymce.php>',
482 $this->generate_htaccess_rule( true ),
483 '</Files>',
484 '<Files ms-files.php>',
485 $this->generate_htaccess_rule( true ),
486 '</Files>',
487 '## WP Defender - End ##',
488 );
489
490 file_put_contents( $ht_access_path, $exists_rules . implode( PHP_EOL, $rule ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
491 }
492
493 /**
494 * Generates the correct Apache rules for allow/deny based on the server version.
495 *
496 * @param bool $allow Specifies whether to generate allow or deny rules.
497 *
498 * @return string Returns the generated Apache rules as a string.
499 */
500 protected function generate_htaccess_rule( $allow = true ) {
501 if ( version_compare( $this->get_version(), 2.4, '>=' ) ) {
502 if ( $allow ) {
503 return 'Require all granted';
504 } else {
505 return 'Require all denied';
506 }
507 } elseif ( $allow ) {
508 return 'Allow from all';
509 } else {
510 return 'Order allow,deny' . PHP_EOL .
511 'Deny from all';
512 }
513 }
514
515 /**
516 * Protect uploads directory.
517 * This only when user provide a custom uploads.
518 *
519 * @return void|WP_Error
520 */
521 public function protect_uploads_directory() {
522 if ( defined( 'UPLOADS' ) ) {
523 $this->contentdir_path = ABSPATH . UPLOADS . '/.htaccess';
524
525 return $this->protect_content_directory();
526 }
527 }
528
529 /**
530 * Unprotect content directory.
531 *
532 * @return void|WP_Error
533 */
534 public function unprotect_content_directory() {
535 global $wp_filesystem;
536 // Initialize the WP filesystem, no more using 'file-put-contents' function.
537 if ( empty( $wp_filesystem ) ) {
538 require_once ABSPATH . '/wp-admin/includes/file.php';
539 WP_Filesystem();
540 }
541 $ht_access_path = $this->contentdir_path;
542
543 if ( empty( $ht_access_path ) ) {
544 $ht_access_path = WP_CONTENT_DIR . '/.htaccess';
545 }
546 // Quick exit if the file does not exist.
547 if ( ! is_file( $ht_access_path ) ) {
548 return;
549 }
550 // If the htaccess-file is existed.
551 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
552 return $this->get_error_by_file_path( $ht_access_path );
553 }
554
555 $ht_config = $this->cleanup_old_rules( $wp_filesystem->get_contents( $ht_access_path ) );
556 $ht_config = trim( $ht_config );
557
558 file_put_contents( $ht_access_path, trim( $ht_config ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
559 }
560
561 /**
562 * Unprotect includes-directory.
563 * Todo: can be combined with unprotect_content_directory()?
564 *
565 * @return void|WP_Error
566 */
567 public function unprotect_includes_directory() {
568 global $wp_filesystem;
569 // Initialize the WP filesystem, no more using 'file-put-contents' function.
570 if ( empty( $wp_filesystem ) ) {
571 require_once ABSPATH . '/wp-admin/includes/file.php';
572 WP_Filesystem();
573 }
574 $ht_access_path = $this->includedir_path;
575
576 if ( empty( $ht_access_path ) ) {
577 $ht_access_path = ABSPATH . WPINC . '/.htaccess';
578 }
579 // Quick exit if the file does not exist.
580 if ( ! is_file( $ht_access_path ) ) {
581 return;
582 }
583 // If the htaccess-file is existed.
584 if ( ! $wp_filesystem->is_writable( $ht_access_path ) ) {
585 return $this->get_error_by_file_path( $ht_access_path );
586 }
587
588 $ht_config = $this->cleanup_old_rules( $wp_filesystem->get_contents( $ht_access_path ) );
589 file_put_contents( $ht_access_path, trim( $ht_config ), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
590 }
591
592 /**
593 * Unprotect upload directory.
594 *
595 * @return void
596 */
597 public function unprotect_upload_directory() {
598 if ( defined( 'UPLOADS' ) ) {
599 $this->contentdir_path = ABSPATH . UPLOADS . '/.htaccess';
600 $this->unprotect_content_directory();
601 }
602 }
603
604 /**
605 * Set the exclude file paths.
606 *
607 * @param array $config to set.
608 */
609 public function set_new_htaccess_config( $config = array() ) {
610 if ( ! empty( $config ) ) {
611 $this->new_htaccess_config = $config;
612 }
613 }
614
615 /**
616 * Get the new HT config.
617 *
618 * @return array - $new_htaccess_config
619 */
620 public function get_new_htaccess_config() {
621 return $this->new_htaccess_config;
622 }
623
624 /**
625 * Cleans up old rules from the existing .htaccess configuration.
626 *
627 * @param string $exists_rules The existing .htaccess rules.
628 *
629 * @return string Returns the cleaned-up .htaccess rules.
630 */
631 public function cleanup_old_rules( $exists_rules ) {
632 $pattern = '/(## WP Defender - Protect PHP Executed ##((.|\n)*)## WP Defender - End ##)/';
633
634 if ( preg_match( $pattern, $exists_rules ) ) {
635 $exists_rules = preg_replace( $pattern, '', $exists_rules );
636 }
637
638 $exists_rules = trim( $exists_rules );
639
640 if ( strlen( $exists_rules ) ) {
641 $exists_rules .= PHP_EOL;
642 }
643
644 return $exists_rules;
645 }
646 }
647