PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.1
Patchstack – WordPress & Plugins Security v2.3.1
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.1, at includes/htaccess.php

339 lines 11.9 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 return $this->plugin->htaccess->self_check( $rules );
164 }
165
166 /**
167 * Get the web-server type.
168 *
169 * @return boolean
170 */
171 public function is_server_supported() {
172 $server = strtolower( $_SERVER['SERVER_SOFTWARE'] );
173 foreach ( [ 'apache', 'nginx', 'litespeed' ] as $webserver ) {
174 if ( strstr( $server, $webserver ) ) {
175 return true;
176 }
177 }
178
179 return false;
180 }
181
182 /**
183 * Write .htaccess directly without causing a server missconfiguration (500)
184 * (PHP will end the process even if the browser-window was closed.)
185 *
186 * @param string $new_rules
187 * @return boolean
188 */
189 public function self_check( $new_rules ) {
190 // Don't continue if we have no rules
191 if ( empty( $new_rules ) ) {
192 return false;
193 }
194 $new_rules = PHP_EOL . PHP_EOL . '# BEGIN Patchstack' . PHP_EOL . $new_rules . PHP_EOL . '# END Patchstack' . PHP_EOL . PHP_EOL;
195
196 // Require the filesystem libraries.
197 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
198 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
199 $fs = new WP_Filesystem_Direct( '' );
200
201 // Don't continue if .htaccess does not exist or cannot be written to.
202 if ( ! $fs->exists( ABSPATH . '.htaccess' ) ) {
203 return false;
204 }
205
206 // Get the current data in the .htaccess file.
207 $current_rules = $old = $fs->get_contents( ABSPATH . '.htaccess' );
208
209 // Delete all Patchstack related stuff so we can properly re-inject it.
210 $current_rules = $this->delete_all_between( '# BEGIN WebARX', '# END WebARX', $current_rules );
211 $current_rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $current_rules );
212 $current_rules = $this->delete_all_between( '# CUSTOM WEBARX RULES', '# END CUSTOM WEBARX RULES', $current_rules );
213 $current_rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $current_rules );
214 $new_rules = $this->delete_all_between( '# BEGIN WordPress', '# END WordPress', $new_rules );
215
216 // Get the custom .htaccess rules, if any are set.
217 $custom = $this->get_custom_rules();
218
219 if ( get_site_option( 'patchstack_firewall_custom_rules_loc', 'bottom' ) == 'top' ) {
220 $new_rules = $custom . $new_rules;
221 } else {
222 $new_rules = $new_rules . $custom;
223 }
224
225 // Determine if the new rules even need to be saved.
226 $rules_hash = sha1( $new_rules );
227 if ( get_site_option( 'patchstack_htaccess_rules_hash', '' ) == $rules_hash && ( stripos( $old, 'begin webarx' ) !== false || stripos( $old, 'begin patchstack' ) !== false ) ) {
228 return false;
229 }
230
231 // Save the new rules and adjust # and newline of our own rules.
232 update_site_option( 'patchstack_htaccess_rules_hash', $rules_hash );
233 $new_rules = preg_replace( "/[\r\n]+/", "\r\n", $new_rules );
234 $new_rules = preg_replace( '/#/', "\r\n#", $new_rules );
235
236 // In order to support all Patchstack plugin versions with newline fix, we have to remove this part ourselves.
237 $new_rules = str_replace( "\r\n\r\n# BEGIN Patchstack", '# BEGIN Patchstack', $new_rules );
238 $new_rules = str_replace( "# END Patchstack\r\n", '# END Patchstack', $new_rules );
239
240 // Remove RewriteBase / from the Patchstack rules.
241 $new_rules = str_replace( "\r\n RewriteBase /", '', $new_rules );
242 $new_rules = str_replace( '/index.php', 'index.php', $new_rules );
243
244 // Merge the rules together.
245 $new_rules = $new_rules . "\n" . $current_rules;
246
247 // Determine if the Patchstack rules starts on its own line.
248 $lines = explode( "\n", $new_rules );
249 foreach ( $lines as $line ) {
250 if ( stripos( $line, 'begin patchstack' ) !== false && trim( strtolower( $line ) ) != '# begin patchstack' ) {
251 $new_rules = str_replace( '# BEGIN Patchstack', "\r\n# BEGIN Patchstack", $new_rules );
252 }
253 }
254
255 // Put the contents into the .htaccess file.
256 $fs->put_contents( ABSPATH . '.htaccess', $new_rules, FS_CHMOD_FILE );
257
258 // Check if the new rules work.
259 // 500 internal server error - did not work. Restore old rules.
260 $status = $this->get_site_status_code();
261 if ( $status == '' || $status >= 500 ) {
262 $fs->put_contents( ABSPATH . '.htaccess', $old, FS_CHMOD_FILE );
263 update_site_option( 'patchstack_firewall_custom_rules', '' );
264 }
265
266 return $status < 500;
267 }
268
269 /**
270 * Retrieve the custom .htaccess rules and inject into the .htaccess file.
271 *
272 * @return string
273 */
274 public function get_custom_rules() {
275 $custom = get_site_option( 'patchstack_firewall_custom_rules', '' );
276 if ( empty( $custom ) || is_array( $custom ) || $custom == 'Array' ) {
277 $custom = '';
278 }
279
280 // Do we have any custom rules to inject?
281 $tmp = '';
282 if ( $custom != '' ) {
283 $tmp = PHP_EOL . '# CUSTOM PATCHSTACK RULES' . PHP_EOL;
284 $tmp .= $custom . PHP_EOL;
285 $tmp .= '# END CUSTOM PATCHSTACK RULES';
286 }
287 return $tmp;
288 }
289
290 /**
291 * Retrieve the status code of the site.
292 * This is done to determine if the .htaccess rules do not trigger an error.
293 *
294 * @return integer
295 */
296 public function get_site_status_code() {
297 $response = wp_remote_get( get_site_url() );
298 $http_code = wp_remote_retrieve_response_code( $response );
299 return $http_code;
300 }
301
302 /**
303 * Remove all Patchstack rules from the .htaccess file.
304 *
305 * @return void
306 */
307 public function cleanup_htaccess_file() {
308 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
309 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
310 $fs = new WP_Filesystem_Direct( '' );
311
312 if ( $fs->exists( ABSPATH . '.htaccess' ) ) {
313 $curdata = $fs->get_contents( ABSPATH . '.htaccess' );
314 $rules = $this->delete_all_between( '# BEGIN Patchstack', '# END Patchstack', $curdata );
315 $rules = $this->delete_all_between( '# CUSTOM PATCHSTACK RULES', '# END CUSTOM PATCHSTACK RULES', $rules );
316 $fs->put_contents( ABSPATH . '.htaccess', $rules, FS_CHMOD_FILE );
317 }
318 }
319
320 /**
321 * Delete characters between a begin and end string.
322 *
323 * @param string $begin
324 * @param string $end
325 * @param string $string
326 * @return string
327 */
328 public function delete_all_between( $begin, $end, $string ) {
329 $begin_pos = strpos( $string, $begin );
330 $end_pos = strpos( $string, $end );
331 if ( $begin_pos === false || $end_pos === false ) {
332 return $string;
333 }
334
335 $delete = substr( $string, $begin_pos, ( $end_pos + strlen( $end ) ) - $begin_pos );
336 return str_replace( $delete, '', $string );
337 }
338 }
339