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

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