PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.6
Patchstack – WordPress & Plugins Security v2.3.6
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / htaccess.php

htaccess.php in Patchstack – WordPress & Plugins Security 2.3.6, at includes/htaccess.php

353 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to perform interactions with the
10 * .htaccess file.
11 */
12 class P_Htaccess extends P_Core {
13
14 /**
15 * Add the actions required for htaccess interactions.
16 *
17 * @param Patchstack $core
18 * @return void
19 */
20 public function __construct( $core ) {
21 parent::__construct( $core );
22
23 if ( $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
24 return;
25 }
26
27 add_action( 'updated_option', [ $this, 'update_option_extras' ], 10, 3 );
28 }
29
30 /**
31 * If option is updated, write to .htaccess file.
32 *
33 * @param string $option_name
34 * @param string $option_name
35 * @param mixed $value
36 * @return void
37 */
38 public function update_option_extras( $option_name, $old_value, $value ) {
39 if ( !in_array( $option_name, [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_pingback_protection', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_trace_and_track', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking', 'patchstack_firewall_custom_rules' ] ) ) {
40 return;
41 }
42
43 if ( $old_value == $value ) {
44 return;
45 }
46
47 $this->plugin->rules->post_firewall_rules();
48 }
49
50 /**
51 * Get the turned on .htaccess firewall settings.
52 *
53 * @return array
54 */
55 public function get_firewall_rule_settings() {
56 $settings = [];
57 $options = [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking', 'patchstack_basicscanblock' ];
58 foreach ( $options as $option ) {
59 if ( get_site_option( $option ) ) {
60 $settings[] = ( $option == 'patchstack_basicscanblock' ? 'webarx_wpscan_block' : str_replace( 'patchstack_', 'webarx_', $option ) );
61 }
62 }
63
64 return $settings;
65 }
66
67 /**
68 * Determine the current state of the firewall.
69 *
70 * @return boolean
71 */
72 public function firewall() {
73 // Get the firewall state.
74 $sum_of_firewall = 0;
75 foreach ( [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_pingback_protection', 'patchstack_block_debug_log_access', 'patchstack_block_fake_bots', 'patchstack_index_views', 'patchstack_trace_and_track', 'patchstack_proxy_comment_posting', 'patchstack_image_hotlinking' ] as $option ) {
76 $value = get_site_option( $option, 0 );
77 $sum_of_firewall += empty( $value ) ? 0 : 1;
78 }
79
80 // Update the options.
81 $onoff = $sum_of_firewall > 1 ? 0 : 1;
82 foreach ( [ 'patchstack_prevent_default_file_access', 'patchstack_basic_firewall', 'patchstack_block_debug_log_access', 'patchstack_index_views', 'patchstack_proxy_comment_posting' ] as $option ) {
83 update_site_option( $option, $onoff );
84 }
85 update_site_option( 'patchstack_block_fake_bots', 0 );
86 update_site_option( 'patchstack_image_hotlinking', 0 );
87
88 // Pull the rules or cleanup the .htaccess file?
89 if ( $onoff == 1 ) {
90 $this->plugin->rules->post_firewall_rules();
91 } else {
92 $this->cleanup_htaccess_file();
93 }
94
95 return true;
96 }
97
98 /**
99 * Write the .htaccess firewall rules to the .htaccess file.
100 *
101 * @param string $rules
102 * @return void
103 */
104 public function write_rules_to_htaccess( $rules = '' ) {
105 if ( ! $this->is_server_supported() || get_site_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) ) {
106 return false;
107 }
108
109 // Determine if the .htaccess file exists.
110 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
111 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
112 $fs = new WP_Filesystem_Direct( '' );
113 if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) {
114 $fs->touch( ABSPATH . '.htaccess' );
115
116 // Don't continue if .htaccess does not exist or cannot be written to.
117 if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) {
118 return false;
119 }
120 }
121
122 // Get the current rules.
123 $current = $old = $fs->get_contents( ABSPATH . '.htaccess' );
124 $current = $this->delete_all_between( '# Patchstack Firewall Start', "# Patchstack Firewall End\r\n", $current );
125
126 // If no rules, then we delete the old ones.
127 if ( $rules != '' ) {
128 $current = "# Patchstack Firewall Start\r\n<IfModule mod_rewrite.c>\r\nRewriteEngine On\r\n" . $rules . "\r\n</IfModule>\r\n# Patchstack Firewall End\r\n" . $current;
129 }
130
131 // Put the contents into the .htaccess file.
132 $fs->put_contents( ABSPATH . '.htaccess', $current, FS_CHMOD_FILE );
133
134 // Check if the new rules work.
135 // 500 internal server error - did not work. Restore old rules.
136 $status = $this->get_site_status_code();
137 if ( $status == '' || $status >= 500 ) {
138 $fs->put_contents( ABSPATH . '.htaccess', $old, FS_CHMOD_FILE );
139 return false;
140 }
141
142 return true;
143 }
144
145 /**
146 * Write given rules to the .htaccess file.
147 *
148 * @param string $rules
149 * @return boolean
150 */
151 public function write_to_htaccess( $rules = '' ) {
152 if ( ! $this->is_server_supported() || get_site_option( 'patchstack_disable_htaccess', 0 ) || ( defined( 'PS_DISABLE_HTACCESS' ) && PS_DISABLE_HTACCESS ) ) {
153 return false;
154 }
155
156 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
157 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
158 $fs = new WP_Filesystem_Direct( '' );
159 if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) {
160 $fs->touch( ABSPATH . '.htaccess' );
161 }
162
163 // Some rule adjustments.
164 $rewrites = [
165 'RedirectMatch 409 .(htaccess|htpasswd|errordocs|logs)$' => 'RedirectMatch 403 \.(htaccess|htpasswd|errordocs|logs)$',
166 "\n RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in.*$ [NC]" => '',
167 "\n RewriteCond %{REMOTE_ADDR} !=18.221.197.243" => '',
168 '^wp-includes/[^/]+.php$' => '^wp-includes/.*\.php$',
169 'RewriteRule ^debug*.*log$ index.php?webarx_fpage=502 [L,QSA]' => 'RewriteRule debug\.log$ index.php?webarx_fpage=502 [L,QSA]',
170 '*.*' => '\.'
171 ];
172
173 foreach ($rewrites as $find => $replace) {
174 $rules = str_replace($find, $replace, $rules);
175 }
176
177 return $this->plugin->htaccess->self_check( $rules );
178 }
179
180 /**
181 * Get the web-server type.
182 *
183 * @return boolean
184 */
185 public function is_server_supported() {
186 $server = strtolower( $_SERVER['SERVER_SOFTWARE'] );
187 foreach ( [ 'apache', 'nginx', 'litespeed' ] as $webserver ) {
188 if ( strstr( $server, $webserver ) ) {
189 return true;
190 }
191 }
192
193 return false;
194 }
195
196 /**
197 * Write .htaccess directly without causing a server missconfiguration (500)
198 * (PHP will end the process even if the browser-window was closed.)
199 *
200 * @param string $new_rules
201 * @return boolean
202 */
203 public function self_check( $new_rules ) {
204 // Don't continue if we have no rules
205 if ( empty( $new_rules ) ) {
206 return false;
207 }
208 $new_rules = PHP_EOL . PHP_EOL . '# BEGIN Patchstack' . PHP_EOL . $new_rules . PHP_EOL . '# END Patchstack' . PHP_EOL . PHP_EOL;
209
210 // Require the filesystem libraries.
211 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
212 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
213 $fs = new WP_Filesystem_Direct( '' );
214
215 // Don't continue if .htaccess does not exist or cannot be written to.
216 if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) {
217 return false;
218 }
219
220 // Get the current data in the .htaccess file.
221 $current_rules = $old = $fs->get_contents( ABSPATH . '.htaccess' );
222
223 // Delete all Patchstack related stuff so we can properly re-inject it.
224 $current_rules = $this->delete_all_between( '# BEGIN WebARX', '# END WebARX', $current_rules );
225 $current_rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $current_rules );
226 $current_rules = $this->delete_all_between( '# CUSTOM WEBARX RULES', '# END CUSTOM WEBARX RULES', $current_rules );
227 $current_rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $current_rules );
228 $new_rules = $this->delete_all_between( '# BEGIN WordPress', '# END WordPress', $new_rules );
229
230 // Get the custom .htaccess rules, if any are set.
231 $custom = $this->get_custom_rules();
232
233 if ( get_site_option( 'patchstack_firewall_custom_rules_loc', 'bottom' ) == 'top' ) {
234 $new_rules = $custom . $new_rules;
235 } else {
236 $new_rules = $new_rules . $custom;
237 }
238
239 // Determine if the new rules even need to be saved.
240 $rules_hash = sha1( $new_rules );
241 if ( get_site_option( 'patchstack_htaccess_rules_hash', '' ) == $rules_hash && ( stripos( $old, 'begin webarx' ) !== false || stripos( $old, 'begin patchstack' ) !== false ) ) {
242 return false;
243 }
244
245 // Save the new rules and adjust # and newline of our own rules.
246 update_site_option( 'patchstack_htaccess_rules_hash', $rules_hash );
247 $new_rules = preg_replace( "/[\r\n]+/", "\r\n", $new_rules );
248 $new_rules = preg_replace( '/#/', "\r\n#", $new_rules );
249
250 // In order to support all Patchstack plugin versions with newline fix, we have to remove this part ourselves.
251 $new_rules = str_replace( "\r\n\r\n# BEGIN Patchstack", '# BEGIN Patchstack', $new_rules );
252 $new_rules = str_replace( "# END Patchstack\r\n", '# END Patchstack', $new_rules );
253
254 // Remove RewriteBase / from the Patchstack rules.
255 $new_rules = str_replace( "\r\n RewriteBase /", '', $new_rules );
256 $new_rules = str_replace( '/index.php', 'index.php', $new_rules );
257
258 // Merge the rules together.
259 $new_rules = $new_rules . "\n" . $current_rules;
260
261 // Determine if the Patchstack rules starts on its own line.
262 $lines = explode( "\n", $new_rules );
263 foreach ( $lines as $line ) {
264 if ( stripos( $line, 'begin patchstack' ) !== false && trim( strtolower( $line ) ) != '# begin patchstack' ) {
265 $new_rules = str_replace( '# BEGIN Patchstack', "\r\n# BEGIN Patchstack", $new_rules );
266 }
267 }
268
269 // Put the contents into the .htaccess file.
270 $fs->put_contents( ABSPATH . '.htaccess', $new_rules, FS_CHMOD_FILE );
271
272 // Check if the new rules work.
273 // 500 internal server error - did not work. Restore old rules.
274 $status = $this->get_site_status_code();
275 if ( $status == '' || $status >= 500 ) {
276 $fs->put_contents( ABSPATH . '.htaccess', $old, FS_CHMOD_FILE );
277 update_site_option( 'patchstack_firewall_custom_rules', '' );
278 }
279
280 return $status < 500;
281 }
282
283 /**
284 * Retrieve the custom .htaccess rules and inject into the .htaccess file.
285 *
286 * @return string
287 */
288 public function get_custom_rules() {
289 $custom = get_site_option( 'patchstack_firewall_custom_rules', '' );
290 if ( empty( $custom ) || is_array( $custom ) || $custom == 'Array' ) {
291 $custom = '';
292 }
293
294 // Do we have any custom rules to inject?
295 $tmp = '';
296 if ( $custom != '' ) {
297 $tmp = PHP_EOL . '# CUSTOM PATCHSTACK RULES' . PHP_EOL;
298 $tmp .= $custom . PHP_EOL;
299 $tmp .= '# END CUSTOM PATCHSTACK RULES';
300 }
301 return $tmp;
302 }
303
304 /**
305 * Retrieve the status code of the site.
306 * This is done to determine if the .htaccess rules do not trigger an error.
307 *
308 * @return integer
309 */
310 public function get_site_status_code() {
311 $response = wp_remote_get( get_site_url() );
312 $http_code = wp_remote_retrieve_response_code( $response );
313 return $http_code;
314 }
315
316 /**
317 * Remove all Patchstack rules from the .htaccess file.
318 *
319 * @return void
320 */
321 public function cleanup_htaccess_file() {
322 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
323 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
324 $fs = new WP_Filesystem_Direct( '' );
325
326 if ( $fs->exists( ABSPATH . '.htaccess' ) ) {
327 $curdata = $fs->get_contents( ABSPATH . '.htaccess' );
328 $rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $curdata );
329 $rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $rules );
330 $fs->put_contents( ABSPATH . '.htaccess', $rules, FS_CHMOD_FILE );
331 }
332 }
333
334 /**
335 * Delete characters between a begin and end string.
336 *
337 * @param string $begin
338 * @param string $end
339 * @param string $string
340 * @return string
341 */
342 public function delete_all_between( $begin, $end, $string ) {
343 $begin_pos = strpos( $string, $begin );
344 $end_pos = strpos( $string, $end );
345 if ( $begin_pos === false || $end_pos === false ) {
346 return $string;
347 }
348
349 $delete = substr( $string, $begin_pos, ( $end_pos + strlen( $end ) ) - $begin_pos );
350 return str_replace( $delete, '', $string );
351 }
352 }
353