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

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