PluginProbe ʕ •ᴥ•ʔ
SiteGuard WP Plugin / 1.8.6-beta1
SiteGuard WP Plugin v1.8.6-beta1
1.8.9 1.8.8 1.8.7 1.8.6 1.8.6-beta1 1.8.6-beta2 1.8.4 1.8.5 1.8.3 1.8.2 1.8.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.4.3 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.11 1.7.12 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.0-beta1 1.8.0-beta2 1.8.0-beta3 1.8.0-beta4
siteguard / classes / siteguard-htaccess.php
siteguard / classes Last commit date
siteguard-admin-filter.php 3 months ago siteguard-base.php 2 months ago siteguard-captcha.php 2 months ago siteguard-config.php 1 year ago siteguard-disable-author-query.php 2 months ago siteguard-disable-pingback.php 3 months ago siteguard-disable-xmlrpc.php 3 months ago siteguard-htaccess.php 2 months ago siteguard-login-alert.php 3 months ago siteguard-login-history.php 2 months ago siteguard-login-lock.php 2 months ago siteguard-rename-login.php 2 months ago siteguard-updates-notify.php 2 months ago siteguard-waf-exclude-rule.php 3 months ago
siteguard-htaccess.php
436 lines
1 <?php
2
3 class SiteGuard_Htaccess extends SiteGuard_Base {
4 const HTACCESS_PERMISSION = 0604;
5 const HTACCESS_MARK_START = '#SITEGUARD_PLUGIN_SETTINGS_START';
6 const HTACCESS_MARK_END = '#SITEGUARD_PLUGIN_SETTINGS_END';
7
8 function __construct() {
9 }
10 static function get_htaccess_file() {
11 return ABSPATH . '.htaccess';
12 }
13 static function get_tmp_dir() {
14 return SITEGUARD_PATH . 'tmp/';
15 }
16 static function is_writable_htaccess() {
17 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && false !== stripos( $_SERVER['SERVER_SOFTWARE'], 'nginx' ) ) {
18 return false;
19 }
20 if ( file_exists( self::get_htaccess_file() ) ) {
21 return is_writable( self::get_htaccess_file() );
22 }
23 return is_writable( ABSPATH );
24 }
25
26 // Diagnostic reason for the most recent test_htaccess() failure, as an array
27 // like array( 'code' => 'http_status', 'url' => ..., 'status' => 403 ). An empty
28 // array means success (or not yet run). SiteGuard_RenameLogin reads this to
29 // record why it fell back to stub (.php) mode, so administrators can see the
30 // cause on the settings screen.
31 public static $last_reason = array();
32
33 static function test_htaccess() {
34 self::$last_reason = array();
35 if ( ! self::is_writable_htaccess() ) {
36 $is_nginx = isset( $_SERVER['SERVER_SOFTWARE'] ) && false !== stripos( $_SERVER['SERVER_SOFTWARE'], 'nginx' );
37 self::$last_reason = array( 'code' => $is_nginx ? 'nginx' : 'not_writable' );
38 return false;
39 }
40
41 // Sweep orphaned test directories left behind by previous runs whose
42 // wp_remote_get timed out before cleanup could execute.
43 self::cleanup_orphaned_test_dirs();
44
45
46 $test_dir_name = 'siteguard-test-' . uniqid();
47 $test_dir_path = ABSPATH . $test_dir_name;
48 $htaccess_path = $test_dir_path . '/.htaccess';
49 $php_file_path = $test_dir_path . '/test.php';
50 $test_url = home_url( '/' . $test_dir_name . '/test.html' );
51 $php_content = '<?php echo "SUCCESS";';
52 $htaccess_content = "RewriteEngine On\nRewriteRule ^test\\.html$ test.php [L]";
53
54 $cleanup = function () use ( $htaccess_path, $php_file_path, $test_dir_path ) {
55 if ( file_exists( $htaccess_path ) ) {
56 @unlink( $htaccess_path );
57 }
58 if ( file_exists( $php_file_path ) ) {
59 @unlink( $php_file_path );
60 }
61 if ( is_dir( $test_dir_path ) ) {
62 @rmdir( $test_dir_path );
63 }
64 };
65
66 if ( ! @mkdir( $test_dir_path, 0755 ) ) {
67 self::$last_reason = array( 'code' => 'mkdir' );
68 return false;
69 }
70
71 if ( false === @file_put_contents( $php_file_path, $php_content ) || false === @file_put_contents( $htaccess_path, $htaccess_content ) ) {
72 $cleanup();
73 self::$last_reason = array( 'code' => 'write' );
74 return false;
75 }
76
77 $args = array(
78 'timeout' => 10,
79 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
80 );
81 $response = wp_remote_get( $test_url, $args );
82
83 // On success the .htaccess rewrite turned test.html into test.php (SUCCESS).
84 if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) && 'SUCCESS' === wp_remote_retrieve_body( $response ) ) {
85 $cleanup();
86 return true;
87 }
88
89 // The rewrite test failed. Probe test.php directly (before cleanup) to tell
90 // apart "the .htaccess was ignored" from "the test files were unreachable":
91 // if test.php itself returns SUCCESS, the directory and PHP are reachable
92 // and only the RewriteRule had no effect (AllowOverride None / mod_rewrite
93 // off). If test.php is also unreachable, the URL did not map to the test
94 // directory at all (subdirectory install, routing, or access restriction).
95 $php_url = home_url( '/' . $test_dir_name . '/test.php' );
96 $php_probe = wp_remote_get( $php_url, $args );
97 $probe_ok = ! is_wp_error( $php_probe ) && 200 === wp_remote_retrieve_response_code( $php_probe ) && 'SUCCESS' === wp_remote_retrieve_body( $php_probe );
98
99 $cleanup();
100
101 if ( is_wp_error( $response ) ) {
102 self::$last_reason = array(
103 'code' => 'wp_error',
104 'url' => $test_url,
105 'detail' => $response->get_error_message(),
106 );
107 return false;
108 }
109 if ( $probe_ok ) {
110 self::$last_reason = array(
111 'code' => 'htaccess_ignored',
112 'url' => $test_url,
113 );
114 return false;
115 }
116 $status = wp_remote_retrieve_response_code( $response );
117 if ( 200 === $status ) {
118 self::$last_reason = array(
119 'code' => 'bad_body',
120 'url' => $test_url,
121 );
122 return false;
123 }
124 self::$last_reason = array(
125 'code' => 'http_status',
126 'url' => $test_url,
127 'status' => $status,
128 );
129 return false;
130 }
131 private static function cleanup_orphaned_test_dirs() {
132 $orphans = glob( ABSPATH . 'siteguard-test-*', GLOB_ONLYDIR );
133 if ( empty( $orphans ) ) {
134 return;
135 }
136 foreach ( $orphans as $dir ) {
137 $entries = @scandir( $dir );
138 if ( is_array( $entries ) ) {
139 foreach ( $entries as $entry ) {
140 if ( '.' === $entry || '..' === $entry ) {
141 continue;
142 }
143 $path = $dir . DIRECTORY_SEPARATOR . $entry;
144 if ( is_file( $path ) ) {
145 @unlink( $path );
146 }
147 }
148 }
149 @rmdir( $dir );
150 }
151 }
152 static function get_htaccess_new_file() {
153 return tempnam( self::get_tmp_dir(), 'htaccess_' );
154 }
155 static function make_tmp_dir() {
156 $dir = self::get_tmp_dir();
157 if ( ! wp_mkdir_p( $dir ) ) {
158 siteguard_error_log( "make tempdir failed: $dir" );
159 return false;
160 }
161 // Defense-in-depth against directory listing if .htaccess is ignored
162 // (e.g. Apache configured with AllowOverride None). Do not chmod the
163 // file — making it read-only blocks WordPress plugin overwrite/upgrade.
164 $index_file = $dir . 'index.html';
165 if ( ! file_exists( $index_file ) ) {
166 @file_put_contents( $index_file, '' );
167 }
168 $htaccess_file = $dir . '.htaccess';
169
170 if ( file_exists( $htaccess_file ) ) {
171 $lines = file( $htaccess_file );
172 $res = preg_grep( '/IfModule authz_core_module/', $lines );
173 if ( ! empty( $res ) ) {
174 return true;
175 }
176 }
177
178 if ( $handle = @fopen( $htaccess_file, 'w' ) ) {
179 fwrite( $handle, '<IfModule authz_core_module>' . "\n" );
180 fwrite( $handle, ' Require all denied' . "\n" );
181 fwrite( $handle, '</IfModule>' . "\n" );
182 fwrite( $handle, '<IfModule !authz_core_module>' . "\n" );
183 fwrite( $handle, ' Order deny,allow' . "\n" );
184 fwrite( $handle, ' Deny from all' . "\n" );
185 fwrite( $handle, '</IfModule>' . "\n" );
186 fclose( $handle );
187 }
188
189 return true;
190 }
191 static function is_exists_setting( $mark ) {
192 $result = false;
193 if ( '' === $mark ) {
194 $mark_start = self::HTACCESS_MARK_START;
195 $mark_end = self::HTACCESS_MARK_END;
196 } else {
197 $mark_start = $mark . '_START';
198 $mark_end = $mark . '_END';
199 }
200 $current_file = self::get_htaccess_file();
201 if ( ! file_exists( $current_file ) ) {
202 return $result;
203 }
204 $fr = @fopen( $current_file, 'r' );
205 if ( null === $fr ) {
206 return $result;
207 }
208 $line_num = 0;
209 $start_line = 0;
210 $end_line = 0;
211 while ( ! feof( $fr ) ) {
212 $line = fgets( $fr, 4096 );
213 ++$line_num;
214 if ( false !== strpos( $line, $mark_start ) ) {
215 $start_line = $line_num;
216 }
217 if ( false !== strpos( $line, $mark_end ) ) {
218 $end_line = $line_num;
219 if ( $start_line > 0 && ( $end_line - $start_line ) > 1 ) {
220 $result = true;
221 }
222 break;
223 }
224 }
225 @fclose( $fr );
226
227 return $result;
228 }
229 static function check_permission( $flag_create = true ) {
230 $file = self::get_htaccess_file();
231 if ( true === $flag_create ) {
232 self::get_apply_permission( $file );
233 }
234 if ( ! is_readable( $file ) ) {
235 siteguard_error_log( "file not readable: $file" );
236 return false;
237 }
238 if ( ! is_writable( $file ) ) {
239 siteguard_error_log( "file not writable: $file" );
240 return false;
241 }
242 $path = pathinfo( $file, PATHINFO_DIRNAME );
243 if ( ! is_writable( $path ) ) {
244 siteguard_error_log( 'directory not writable: ' . $path );
245 return false;
246 }
247 return true;
248 }
249 static function get_apply_permission_itr( $file ) {
250 clearstatcache();
251 $perm = intval( substr( sprintf( '%o', fileperms( $file ) ), -4 ), 8 );
252 return $perm;
253 }
254 static function get_apply_permission( $file ) {
255 $perm = self::HTACCESS_PERMISSION;
256 if ( file_exists( $file ) ) {
257 $perm = self::get_apply_permission_itr( $file );
258 } else {
259 @touch( $file );
260 }
261 @chmod( $file, $perm );
262 return $perm;
263 }
264 static function clear_settings( $mark ) {
265 // On Nginx (or any environment where .htaccess is not in use), the
266 // rebuild is a no-op. Skipping here avoids creating the in-plugin
267 // tmp/ directory and any tempnam fragments that would not be
268 // protected from web access.
269 if ( ! self::is_writable_htaccess() ) {
270 return true;
271 }
272 if ( ! self::make_tmp_dir() ) {
273 return false;
274 }
275 if ( '' === $mark ) {
276 $mark_start = self::HTACCESS_MARK_START;
277 $mark_end = self::HTACCESS_MARK_END;
278 } else {
279 $mark_start = $mark . '_START';
280 $mark_end = $mark . '_END';
281 }
282 $flag_settings = false;
283 $current_file = self::get_htaccess_file();
284 if ( ! file_exists( $current_file ) ) {
285 return false;
286 }
287 $perm = self::get_apply_permission( $current_file );
288
289 if ( ! self::check_permission( false ) ) {
290 return false;
291 }
292 $fr = @fopen( $current_file, 'r' );
293 if ( null === $fr ) {
294 siteguard_error_log( "fopen failed: $current_file" );
295 return false;
296 }
297 $new_file = self::get_htaccess_new_file();
298 $fw = @fopen( $new_file, 'w' );
299 if ( null === $fw ) {
300 siteguard_error_log( "fopen failed: $new_file" );
301 @unlink( $new_file );
302 fclose( $fr );
303 return false;
304 }
305 while ( ! feof( $fr ) ) {
306 $line = fgets( $fr, 4096 );
307 if ( false !== strpos( $line, $mark_start ) ) {
308 $flag_settings = true;
309 }
310 if ( false === $flag_settings ) {
311 fputs( $fw, $line, 4096 );
312 }
313 if ( true == $flag_settings && false !== strpos( $line, $mark_end ) ) {
314 $flag_settings = false;
315 }
316 }
317 fclose( $fr );
318 fclose( $fw );
319 @chmod( $new_file, $perm );
320 if ( ! rename( $new_file, $current_file ) ) {
321 siteguard_error_log( "rename failed: $new_file $current_file" );
322 @unlink( $new_file );
323 return false;
324 }
325 return true;
326 }
327 function update_settings( $mark, $data ) {
328 // See note in clear_settings(): skip on Nginx where .htaccess is unused.
329 if ( ! self::is_writable_htaccess() ) {
330 return true;
331 }
332 if ( ! self::make_tmp_dir() ) {
333 return false;
334 }
335 $flag_write = false;
336 $flag_through = true;
337 $flag_wp = false;
338 $flag_wp_set = false;
339 $wp_settings = '';
340 $mark_start = $mark . '_START';
341 $mark_end = $mark . '_END';
342 $mark_wp_start = '# BEGIN WordPress';
343 $mark_wp_end = '# END WordPress';
344 $current_file = self::get_htaccess_file();
345 $perm = self::get_apply_permission( $current_file );
346 if ( ! self::check_permission( false ) ) {
347 return false;
348 }
349 $fr = @fopen( $current_file, 'r' );
350 if ( null === $fr ) {
351 siteguard_error_log( "fopen failed: $current_file" );
352 return false;
353 }
354 $new_file = self::get_htaccess_new_file();
355 if ( ! is_writable( $new_file ) ) {
356 siteguard_error_log( "file not writable: $new_file" );
357 @unlink( $new_file );
358 fclose( $fr );
359 return false;
360 }
361 $fw = @fopen( $new_file, 'w' );
362 if ( null === $fw ) {
363 siteguard_error_log( "fopen failed: $new_file" );
364 @unlink( $new_file );
365 fclose( $fr );
366 return false;
367 }
368 while ( ! feof( $fr ) ) {
369 $line = fgets( $fr, 4096 );
370
371 // Save WordPress settings.
372 // WordPress settings has to be written after SiteGuard settings.
373 if ( false === $flag_write && false == $flag_wp_set && false !== strpos( $line, $mark_wp_start ) ) {
374 $flag_wp = true;
375 $flag_wp_set = true;
376 }
377 if ( $flag_wp_set ) {
378 $wp_settings .= $line;
379 if ( false !== strpos( $line, $mark_wp_end ) ) {
380 $flag_wp_set = false;
381 }
382 continue;
383 }
384
385 if ( false === $flag_write && false !== strpos( $line, $mark_start ) ) {
386 fwrite( $fw, $line, strlen( $line ) );
387 fwrite( $fw, $data, strlen( $data ) );
388 $flag_write = true;
389 $flag_through = false;
390 // continue;
391 }
392 if ( false === $flag_write && false !== strpos( $line, self::HTACCESS_MARK_END ) ) {
393 fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 );
394 fwrite( $fw, $data, strlen( $data ) );
395 fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 );
396 $flag_write = true;
397 }
398 if ( false === $flag_through && false !== strpos( $line, $mark_end ) ) {
399 $flag_through = true;
400 }
401 if ( $flag_through ) {
402 fwrite( $fw, $line, strlen( $line ) );
403 if ( false === $flag_wp && false !== strpos( $line, $mark_wp_start ) ) {
404 $flag_wp = true;
405 }
406 }
407 }
408 if ( false === $flag_write ) {
409 fwrite( $fw, "\n" . self::HTACCESS_MARK_START . "\n", strlen( self::HTACCESS_MARK_START ) + 2 );
410 fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 );
411 fwrite( $fw, $data, strlen( $data ) );
412 fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 );
413 fwrite( $fw, self::HTACCESS_MARK_END . "\n", strlen( self::HTACCESS_MARK_END ) + 1 );
414 }
415 if ( '' != $wp_settings ) { // Write saved WordPress Settings
416 fwrite( $fw, "\n", 1 );
417 fwrite( $fw, $wp_settings, strlen( $wp_settings ) );
418 fwrite( $fw, "\n", 1 );
419 } elseif ( false === $flag_wp ) { // Write empty WordPress Settings
420 fwrite( $fw, "\n", 1 );
421 fwrite( $fw, $mark_wp_start . "\n", strlen( $mark_wp_start ) + 1 );
422 fwrite( $fw, $mark_wp_end . "\n", strlen( $mark_wp_end ) + 1 );
423 fwrite( $fw, "\n", 1 );
424 }
425 fclose( $fr );
426 fclose( $fw );
427 @chmod( $new_file, $perm );
428 if ( ! rename( $new_file, $current_file ) ) {
429 siteguard_error_log( "rename failed: $new_file $current_file" );
430 @unlink( $new_file );
431 return false;
432 }
433 return true;
434 }
435 }
436