siteguard-admin-filter.php
1 month ago
siteguard-base.php
1 month ago
siteguard-captcha.php
1 month ago
siteguard-config.php
11 months ago
siteguard-disable-author-query.php
2 weeks ago
siteguard-disable-pingback.php
1 month ago
siteguard-disable-xmlrpc.php
1 month ago
siteguard-htaccess.php
2 weeks ago
siteguard-login-alert.php
1 month ago
siteguard-login-history.php
1 month ago
siteguard-login-lock.php
1 month ago
siteguard-rename-login.php
1 week ago
siteguard-updates-notify.php
1 month ago
siteguard-waf-exclude-rule.php
1 month ago
siteguard-htaccess.php
444 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 | // The test directory is created under ABSPATH, which is served at the |
| 51 | // WordPress Address (siteurl), NOT necessarily the Site Address (home). |
| 52 | // On "WordPress in its own directory" installs (e.g. core in /wp, site at |
| 53 | // root) these differ, so home_url() would build a URL that does not map to |
| 54 | // the test directory and the self-test would always 404. Use the raw |
| 55 | // siteurl (get_option avoids the rename-login site_url filter) so the URL |
| 56 | // matches ABSPATH. For ordinary installs siteurl == home, so no change. |
| 57 | $base_url = rtrim( get_option( 'siteurl' ), '/' ); |
| 58 | $test_url = $base_url . '/' . $test_dir_name . '/test.html'; |
| 59 | $php_content = '<?php echo "SUCCESS";'; |
| 60 | $htaccess_content = "RewriteEngine On\nRewriteRule ^test\\.html$ test.php [L]"; |
| 61 | |
| 62 | $cleanup = function () use ( $htaccess_path, $php_file_path, $test_dir_path ) { |
| 63 | if ( file_exists( $htaccess_path ) ) { |
| 64 | @unlink( $htaccess_path ); |
| 65 | } |
| 66 | if ( file_exists( $php_file_path ) ) { |
| 67 | @unlink( $php_file_path ); |
| 68 | } |
| 69 | if ( is_dir( $test_dir_path ) ) { |
| 70 | @rmdir( $test_dir_path ); |
| 71 | } |
| 72 | }; |
| 73 | |
| 74 | if ( ! @mkdir( $test_dir_path, 0755 ) ) { |
| 75 | self::$last_reason = array( 'code' => 'mkdir' ); |
| 76 | return false; |
| 77 | } |
| 78 | |
| 79 | if ( false === @file_put_contents( $php_file_path, $php_content ) || false === @file_put_contents( $htaccess_path, $htaccess_content ) ) { |
| 80 | $cleanup(); |
| 81 | self::$last_reason = array( 'code' => 'write' ); |
| 82 | return false; |
| 83 | } |
| 84 | |
| 85 | $args = array( |
| 86 | 'timeout' => 10, |
| 87 | 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), |
| 88 | ); |
| 89 | $response = wp_remote_get( $test_url, $args ); |
| 90 | |
| 91 | // On success the .htaccess rewrite turned test.html into test.php (SUCCESS). |
| 92 | if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) && 'SUCCESS' === wp_remote_retrieve_body( $response ) ) { |
| 93 | $cleanup(); |
| 94 | return true; |
| 95 | } |
| 96 | |
| 97 | // The rewrite test failed. Probe test.php directly (before cleanup) to tell |
| 98 | // apart "the .htaccess was ignored" from "the test files were unreachable": |
| 99 | // if test.php itself returns SUCCESS, the directory and PHP are reachable |
| 100 | // and only the RewriteRule had no effect (AllowOverride None / mod_rewrite |
| 101 | // off). If test.php is also unreachable, the URL did not map to the test |
| 102 | // directory at all (subdirectory install, routing, or access restriction). |
| 103 | $php_url = $base_url . '/' . $test_dir_name . '/test.php'; |
| 104 | $php_probe = wp_remote_get( $php_url, $args ); |
| 105 | $probe_ok = ! is_wp_error( $php_probe ) && 200 === wp_remote_retrieve_response_code( $php_probe ) && 'SUCCESS' === wp_remote_retrieve_body( $php_probe ); |
| 106 | |
| 107 | $cleanup(); |
| 108 | |
| 109 | if ( is_wp_error( $response ) ) { |
| 110 | self::$last_reason = array( |
| 111 | 'code' => 'wp_error', |
| 112 | 'url' => $test_url, |
| 113 | 'detail' => $response->get_error_message(), |
| 114 | ); |
| 115 | return false; |
| 116 | } |
| 117 | if ( $probe_ok ) { |
| 118 | self::$last_reason = array( |
| 119 | 'code' => 'htaccess_ignored', |
| 120 | 'url' => $test_url, |
| 121 | ); |
| 122 | return false; |
| 123 | } |
| 124 | $status = wp_remote_retrieve_response_code( $response ); |
| 125 | if ( 200 === $status ) { |
| 126 | self::$last_reason = array( |
| 127 | 'code' => 'bad_body', |
| 128 | 'url' => $test_url, |
| 129 | ); |
| 130 | return false; |
| 131 | } |
| 132 | self::$last_reason = array( |
| 133 | 'code' => 'http_status', |
| 134 | 'url' => $test_url, |
| 135 | 'status' => $status, |
| 136 | ); |
| 137 | return false; |
| 138 | } |
| 139 | private static function cleanup_orphaned_test_dirs() { |
| 140 | $orphans = glob( ABSPATH . 'siteguard-test-*', GLOB_ONLYDIR ); |
| 141 | if ( empty( $orphans ) ) { |
| 142 | return; |
| 143 | } |
| 144 | foreach ( $orphans as $dir ) { |
| 145 | $entries = @scandir( $dir ); |
| 146 | if ( is_array( $entries ) ) { |
| 147 | foreach ( $entries as $entry ) { |
| 148 | if ( '.' === $entry || '..' === $entry ) { |
| 149 | continue; |
| 150 | } |
| 151 | $path = $dir . DIRECTORY_SEPARATOR . $entry; |
| 152 | if ( is_file( $path ) ) { |
| 153 | @unlink( $path ); |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | @rmdir( $dir ); |
| 158 | } |
| 159 | } |
| 160 | static function get_htaccess_new_file() { |
| 161 | return tempnam( self::get_tmp_dir(), 'htaccess_' ); |
| 162 | } |
| 163 | static function make_tmp_dir() { |
| 164 | $dir = self::get_tmp_dir(); |
| 165 | if ( ! wp_mkdir_p( $dir ) ) { |
| 166 | siteguard_error_log( "make tempdir failed: $dir" ); |
| 167 | return false; |
| 168 | } |
| 169 | // Defense-in-depth against directory listing if .htaccess is ignored |
| 170 | // (e.g. Apache configured with AllowOverride None). Do not chmod the |
| 171 | // file — making it read-only blocks WordPress plugin overwrite/upgrade. |
| 172 | $index_file = $dir . 'index.html'; |
| 173 | if ( ! file_exists( $index_file ) ) { |
| 174 | @file_put_contents( $index_file, '' ); |
| 175 | } |
| 176 | $htaccess_file = $dir . '.htaccess'; |
| 177 | |
| 178 | if ( file_exists( $htaccess_file ) ) { |
| 179 | $lines = file( $htaccess_file ); |
| 180 | $res = preg_grep( '/IfModule authz_core_module/', $lines ); |
| 181 | if ( ! empty( $res ) ) { |
| 182 | return true; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | if ( $handle = @fopen( $htaccess_file, 'w' ) ) { |
| 187 | fwrite( $handle, '<IfModule authz_core_module>' . "\n" ); |
| 188 | fwrite( $handle, ' Require all denied' . "\n" ); |
| 189 | fwrite( $handle, '</IfModule>' . "\n" ); |
| 190 | fwrite( $handle, '<IfModule !authz_core_module>' . "\n" ); |
| 191 | fwrite( $handle, ' Order deny,allow' . "\n" ); |
| 192 | fwrite( $handle, ' Deny from all' . "\n" ); |
| 193 | fwrite( $handle, '</IfModule>' . "\n" ); |
| 194 | fclose( $handle ); |
| 195 | } |
| 196 | |
| 197 | return true; |
| 198 | } |
| 199 | static function is_exists_setting( $mark ) { |
| 200 | $result = false; |
| 201 | if ( '' === $mark ) { |
| 202 | $mark_start = self::HTACCESS_MARK_START; |
| 203 | $mark_end = self::HTACCESS_MARK_END; |
| 204 | } else { |
| 205 | $mark_start = $mark . '_START'; |
| 206 | $mark_end = $mark . '_END'; |
| 207 | } |
| 208 | $current_file = self::get_htaccess_file(); |
| 209 | if ( ! file_exists( $current_file ) ) { |
| 210 | return $result; |
| 211 | } |
| 212 | $fr = @fopen( $current_file, 'r' ); |
| 213 | if ( null === $fr ) { |
| 214 | return $result; |
| 215 | } |
| 216 | $line_num = 0; |
| 217 | $start_line = 0; |
| 218 | $end_line = 0; |
| 219 | while ( ! feof( $fr ) ) { |
| 220 | $line = fgets( $fr, 4096 ); |
| 221 | ++$line_num; |
| 222 | if ( false !== strpos( $line, $mark_start ) ) { |
| 223 | $start_line = $line_num; |
| 224 | } |
| 225 | if ( false !== strpos( $line, $mark_end ) ) { |
| 226 | $end_line = $line_num; |
| 227 | if ( $start_line > 0 && ( $end_line - $start_line ) > 1 ) { |
| 228 | $result = true; |
| 229 | } |
| 230 | break; |
| 231 | } |
| 232 | } |
| 233 | @fclose( $fr ); |
| 234 | |
| 235 | return $result; |
| 236 | } |
| 237 | static function check_permission( $flag_create = true ) { |
| 238 | $file = self::get_htaccess_file(); |
| 239 | if ( true === $flag_create ) { |
| 240 | self::get_apply_permission( $file ); |
| 241 | } |
| 242 | if ( ! is_readable( $file ) ) { |
| 243 | siteguard_error_log( "file not readable: $file" ); |
| 244 | return false; |
| 245 | } |
| 246 | if ( ! is_writable( $file ) ) { |
| 247 | siteguard_error_log( "file not writable: $file" ); |
| 248 | return false; |
| 249 | } |
| 250 | $path = pathinfo( $file, PATHINFO_DIRNAME ); |
| 251 | if ( ! is_writable( $path ) ) { |
| 252 | siteguard_error_log( 'directory not writable: ' . $path ); |
| 253 | return false; |
| 254 | } |
| 255 | return true; |
| 256 | } |
| 257 | static function get_apply_permission_itr( $file ) { |
| 258 | clearstatcache(); |
| 259 | $perm = intval( substr( sprintf( '%o', fileperms( $file ) ), -4 ), 8 ); |
| 260 | return $perm; |
| 261 | } |
| 262 | static function get_apply_permission( $file ) { |
| 263 | $perm = self::HTACCESS_PERMISSION; |
| 264 | if ( file_exists( $file ) ) { |
| 265 | $perm = self::get_apply_permission_itr( $file ); |
| 266 | } else { |
| 267 | @touch( $file ); |
| 268 | } |
| 269 | @chmod( $file, $perm ); |
| 270 | return $perm; |
| 271 | } |
| 272 | static function clear_settings( $mark ) { |
| 273 | // On Nginx (or any environment where .htaccess is not in use), the |
| 274 | // rebuild is a no-op. Skipping here avoids creating the in-plugin |
| 275 | // tmp/ directory and any tempnam fragments that would not be |
| 276 | // protected from web access. |
| 277 | if ( ! self::is_writable_htaccess() ) { |
| 278 | return true; |
| 279 | } |
| 280 | if ( ! self::make_tmp_dir() ) { |
| 281 | return false; |
| 282 | } |
| 283 | if ( '' === $mark ) { |
| 284 | $mark_start = self::HTACCESS_MARK_START; |
| 285 | $mark_end = self::HTACCESS_MARK_END; |
| 286 | } else { |
| 287 | $mark_start = $mark . '_START'; |
| 288 | $mark_end = $mark . '_END'; |
| 289 | } |
| 290 | $flag_settings = false; |
| 291 | $current_file = self::get_htaccess_file(); |
| 292 | if ( ! file_exists( $current_file ) ) { |
| 293 | return false; |
| 294 | } |
| 295 | $perm = self::get_apply_permission( $current_file ); |
| 296 | |
| 297 | if ( ! self::check_permission( false ) ) { |
| 298 | return false; |
| 299 | } |
| 300 | $fr = @fopen( $current_file, 'r' ); |
| 301 | if ( null === $fr ) { |
| 302 | siteguard_error_log( "fopen failed: $current_file" ); |
| 303 | return false; |
| 304 | } |
| 305 | $new_file = self::get_htaccess_new_file(); |
| 306 | $fw = @fopen( $new_file, 'w' ); |
| 307 | if ( null === $fw ) { |
| 308 | siteguard_error_log( "fopen failed: $new_file" ); |
| 309 | @unlink( $new_file ); |
| 310 | fclose( $fr ); |
| 311 | return false; |
| 312 | } |
| 313 | while ( ! feof( $fr ) ) { |
| 314 | $line = fgets( $fr, 4096 ); |
| 315 | if ( false !== strpos( $line, $mark_start ) ) { |
| 316 | $flag_settings = true; |
| 317 | } |
| 318 | if ( false === $flag_settings ) { |
| 319 | fputs( $fw, $line, 4096 ); |
| 320 | } |
| 321 | if ( true == $flag_settings && false !== strpos( $line, $mark_end ) ) { |
| 322 | $flag_settings = false; |
| 323 | } |
| 324 | } |
| 325 | fclose( $fr ); |
| 326 | fclose( $fw ); |
| 327 | @chmod( $new_file, $perm ); |
| 328 | if ( ! rename( $new_file, $current_file ) ) { |
| 329 | siteguard_error_log( "rename failed: $new_file $current_file" ); |
| 330 | @unlink( $new_file ); |
| 331 | return false; |
| 332 | } |
| 333 | return true; |
| 334 | } |
| 335 | function update_settings( $mark, $data ) { |
| 336 | // See note in clear_settings(): skip on Nginx where .htaccess is unused. |
| 337 | if ( ! self::is_writable_htaccess() ) { |
| 338 | return true; |
| 339 | } |
| 340 | if ( ! self::make_tmp_dir() ) { |
| 341 | return false; |
| 342 | } |
| 343 | $flag_write = false; |
| 344 | $flag_through = true; |
| 345 | $flag_wp = false; |
| 346 | $flag_wp_set = false; |
| 347 | $wp_settings = ''; |
| 348 | $mark_start = $mark . '_START'; |
| 349 | $mark_end = $mark . '_END'; |
| 350 | $mark_wp_start = '# BEGIN WordPress'; |
| 351 | $mark_wp_end = '# END WordPress'; |
| 352 | $current_file = self::get_htaccess_file(); |
| 353 | $perm = self::get_apply_permission( $current_file ); |
| 354 | if ( ! self::check_permission( false ) ) { |
| 355 | return false; |
| 356 | } |
| 357 | $fr = @fopen( $current_file, 'r' ); |
| 358 | if ( null === $fr ) { |
| 359 | siteguard_error_log( "fopen failed: $current_file" ); |
| 360 | return false; |
| 361 | } |
| 362 | $new_file = self::get_htaccess_new_file(); |
| 363 | if ( ! is_writable( $new_file ) ) { |
| 364 | siteguard_error_log( "file not writable: $new_file" ); |
| 365 | @unlink( $new_file ); |
| 366 | fclose( $fr ); |
| 367 | return false; |
| 368 | } |
| 369 | $fw = @fopen( $new_file, 'w' ); |
| 370 | if ( null === $fw ) { |
| 371 | siteguard_error_log( "fopen failed: $new_file" ); |
| 372 | @unlink( $new_file ); |
| 373 | fclose( $fr ); |
| 374 | return false; |
| 375 | } |
| 376 | while ( ! feof( $fr ) ) { |
| 377 | $line = fgets( $fr, 4096 ); |
| 378 | |
| 379 | // Save WordPress settings. |
| 380 | // WordPress settings has to be written after SiteGuard settings. |
| 381 | if ( false === $flag_write && false == $flag_wp_set && false !== strpos( $line, $mark_wp_start ) ) { |
| 382 | $flag_wp = true; |
| 383 | $flag_wp_set = true; |
| 384 | } |
| 385 | if ( $flag_wp_set ) { |
| 386 | $wp_settings .= $line; |
| 387 | if ( false !== strpos( $line, $mark_wp_end ) ) { |
| 388 | $flag_wp_set = false; |
| 389 | } |
| 390 | continue; |
| 391 | } |
| 392 | |
| 393 | if ( false === $flag_write && false !== strpos( $line, $mark_start ) ) { |
| 394 | fwrite( $fw, $line, strlen( $line ) ); |
| 395 | fwrite( $fw, $data, strlen( $data ) ); |
| 396 | $flag_write = true; |
| 397 | $flag_through = false; |
| 398 | // continue; |
| 399 | } |
| 400 | if ( false === $flag_write && false !== strpos( $line, self::HTACCESS_MARK_END ) ) { |
| 401 | fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 ); |
| 402 | fwrite( $fw, $data, strlen( $data ) ); |
| 403 | fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 ); |
| 404 | $flag_write = true; |
| 405 | } |
| 406 | if ( false === $flag_through && false !== strpos( $line, $mark_end ) ) { |
| 407 | $flag_through = true; |
| 408 | } |
| 409 | if ( $flag_through ) { |
| 410 | fwrite( $fw, $line, strlen( $line ) ); |
| 411 | if ( false === $flag_wp && false !== strpos( $line, $mark_wp_start ) ) { |
| 412 | $flag_wp = true; |
| 413 | } |
| 414 | } |
| 415 | } |
| 416 | if ( false === $flag_write ) { |
| 417 | fwrite( $fw, "\n" . self::HTACCESS_MARK_START . "\n", strlen( self::HTACCESS_MARK_START ) + 2 ); |
| 418 | fwrite( $fw, $mark_start . "\n", strlen( $mark_start ) + 1 ); |
| 419 | fwrite( $fw, $data, strlen( $data ) ); |
| 420 | fwrite( $fw, $mark_end . "\n", strlen( $mark_end ) + 1 ); |
| 421 | fwrite( $fw, self::HTACCESS_MARK_END . "\n", strlen( self::HTACCESS_MARK_END ) + 1 ); |
| 422 | } |
| 423 | if ( '' != $wp_settings ) { // Write saved WordPress Settings |
| 424 | fwrite( $fw, "\n", 1 ); |
| 425 | fwrite( $fw, $wp_settings, strlen( $wp_settings ) ); |
| 426 | fwrite( $fw, "\n", 1 ); |
| 427 | } elseif ( false === $flag_wp ) { // Write empty WordPress Settings |
| 428 | fwrite( $fw, "\n", 1 ); |
| 429 | fwrite( $fw, $mark_wp_start . "\n", strlen( $mark_wp_start ) + 1 ); |
| 430 | fwrite( $fw, $mark_wp_end . "\n", strlen( $mark_wp_end ) + 1 ); |
| 431 | fwrite( $fw, "\n", 1 ); |
| 432 | } |
| 433 | fclose( $fr ); |
| 434 | fclose( $fw ); |
| 435 | @chmod( $new_file, $perm ); |
| 436 | if ( ! rename( $new_file, $current_file ) ) { |
| 437 | siteguard_error_log( "rename failed: $new_file $current_file" ); |
| 438 | @unlink( $new_file ); |
| 439 | return false; |
| 440 | } |
| 441 | return true; |
| 442 | } |
| 443 | } |
| 444 |