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