debug_logger->log_debug("Unable to write to .htaccess - server type not supported.", 4); return false; //unable to write to the file } //clean up old rules first if (-1 == AIOWPSecurity_Utility_Htaccess::delete_from_htaccess()) { $aio_wp_security->debug_logger->log_debug("Delete operation of .htaccess file failed.", 4); return false; //unable to write to the file } $home_path = AIOWPSecurity_Utility_File::get_home_path(); $htaccess = $home_path . '.htaccess'; $f = @fopen($htaccess, 'a+'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (!$f) { @chmod($htaccess, 0644); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below $f = @fopen($htaccess, 'a+'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (!$f) { $aio_wp_security->debug_logger->log_debug("chmod operation on .htaccess failed.", 4); return false; } } AIOWPSecurity_Utility_File::backup_and_rename_htaccess($htaccess); // TODO - we dont want to continually be backing up the htaccess file $ht = explode(PHP_EOL, implode('', file($htaccess))); // parse each line of file into array $rules = AIOWPSecurity_Utility_Htaccess::getrules(); $rulesarray = explode(PHP_EOL, $rules); $rulesarray = apply_filters('aiowps_htaccess_rules_before_writing', $rulesarray); $contents = array_merge($rulesarray, $ht); $f = @fopen($htaccess, 'w+'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (!$f) { $aio_wp_security->debug_logger->log_debug("Write operation on .htaccess failed.", 4); return false; //we can't write to the file } $blank = false; //write each line to file foreach ($contents as $insertline) { if (trim($insertline) == '') { if (false == $blank) { fwrite($f, PHP_EOL . trim($insertline)); } $blank = true; } else { $blank = false; fwrite($f, PHP_EOL . trim($insertline)); } } if (is_resource($f)) @fclose($f); return true; //success } /** * This function will delete the code which has been added to the .htaccess file by this plugin * It will try to find the comment markers "# BEGIN All In One WP Security" and "# END All In One WP Security" and delete contents in between * * @param string $section * @return Integer{-1,1} -1 for failure, 1 for success. */ public static function delete_from_htaccess($section = 'All In One WP Security') { $home_path = AIOWPSecurity_Utility_File::get_home_path(); $htaccess = $home_path . '.htaccess'; if (!file_exists($htaccess)) { $ht = @fopen($htaccess, 'a+');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (false === $ht) { global $aio_wp_security; $aio_wp_security->debug_logger->log_debug('Failed to create .htaccess file', 4); return -1; } if (is_resource($ht)) @fclose($ht); } // Bug Fix: On some environments such as windows (xampp) this function was clobbering the non-aiowps-related .htaccess contents for certain cases. // In some cases when WordPress saves the .htaccess file (eg, when saving permalink settings), // the line endings differ from the expected PHP_EOL endings. (WordPress saves with "\n" (UNIX style) but PHP_EOL may be set as "\r\n" (WIN/DOS)) // In this case exploding via PHP_EOL may not yield the result we expect. // Therefore we need to do the following extra checks. $ht_contents_imploded = implode('', file($htaccess)); if (empty($ht_contents_imploded)) { return 1; } elseif (strstr($ht_contents_imploded, PHP_EOL)) { $ht_contents = explode(PHP_EOL, $ht_contents_imploded); //parse each line of file into array } elseif (strstr($ht_contents_imploded, "\n")) { $ht_contents = explode("\n", $ht_contents_imploded); //parse each line of file into array } elseif (strstr($ht_contents_imploded, "\r")) { $ht_contents = explode("\r", $ht_contents_imploded); //parse each line of file into array } elseif (strstr($ht_contents_imploded, "\r\n")) { $ht_contents = explode("\r\n", $ht_contents_imploded); //parse each line of file into array } if ($ht_contents) { //as long as there are lines in the file $state = true; $f = @fopen($htaccess, 'w+'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (!$f) { @chmod($htaccess, 0644);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below $f = @fopen($htaccess, 'w+'); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- ignore warning as we try to handle it below if (!$f) return -1; } foreach ($ht_contents as $markerline) { //for each line in the file if (strpos($markerline, '# BEGIN ' . $section) !== false) { //if we're at the beginning of the section $state = false; } if (true == $state) { //as long as we're not in the section keep writing fwrite($f, trim($markerline) . PHP_EOL); } if (strpos($markerline, '# END ' . $section) !== false) { //see if we're at the end of the section $state = true; } } if (is_resource($f)) @fclose($f); return 1; } return 1; } public static function getrules() { global $aio_wp_security; $rules = ""; $rules .= AIOWPSecurity_Utility_Htaccess::getrules_block_wp_file_access(); $rules .= AIOWPSecurity_Utility_Htaccess::getrules_basic_htaccess(); $rules .= AIOWPSecurity_Utility_Htaccess::getrules_block_debug_log_access_htaccess(); $rules .= AIOWPSecurity_Utility_Htaccess::getrules_disable_index_views(); $rules .= AIOWPSecurity_Utility_Htaccess::getrules_disable_trace_and_track(); $rules .= AIOWPSecurity_Utility_Htaccess::getrules_5g_blacklist(); $rules .= AIOWPSecurity_Utility_Htaccess::prevent_image_hotlinks(); $custom_rules = AIOWPSecurity_Utility_Htaccess::getrules_custom_rules(); if ($aio_wp_security->configs->get_value('aiowps_place_custom_rules_at_top')=='1') { $rules = $custom_rules . $rules; } else { $rules .= $custom_rules; } //TODO: The following utility functions are ready to use when we write the menu pages for these features //Add more functions for features as needed //$rules .= AIOWPSecurity_Utility_Htaccess::getrules_somefeature(); //Add outer markers if we have rules if ('' != $rules) { $rules = "# BEGIN All In One WP Security" . PHP_EOL . $rules . "# END All In One WP Security" . PHP_EOL; } return $rules; } /** * This function will write rules to prevent people from accessing the following files: * readme.html, license.txt and wp-config-sample.php. */ public static function getrules_block_wp_file_access() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_prevent_default_wp_file_access') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$prevent_wp_file_access_marker_start . PHP_EOL; //Add feature marker start $rules .= self::create_apache2_access_denied_rule('license.txt'); $rules .= self::create_apache2_access_denied_rule('wp-config-sample.php'); $rules .= self::create_apache2_access_denied_rule('readme.html'); $rules .= AIOWPSecurity_Utility_Htaccess::$prevent_wp_file_access_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * TODO - info */ public static function getrules_basic_htaccess() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_enable_basic_firewall') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$basic_htaccess_rules_marker_start . PHP_EOL; //Add feature marker start //protect the htaccess file - this is done by default with apache config file but we are including it here for good measure $rules .= self::create_apache2_access_denied_rule('.htaccess'); //disable the server signature $rules .= 'ServerSignature Off' . PHP_EOL; //limit file upload size $upload_limit = $aio_wp_security->configs->get_value('aiowps_max_file_upload_size'); //Shouldn't be empty but just in case $upload_limit = empty($upload_limit) ? AIOS_FIREWALL_MAX_FILE_UPLOAD_LIMIT_MB : $upload_limit; $upload_limit = $upload_limit * 1024 * 1024; // Convert from MB to Bytes - approx but close enough $rules .= 'LimitRequestBody '.$upload_limit . PHP_EOL; // protect wpconfig.php. $rules .= self::create_apache2_access_denied_rule('wp-config.php'); $rules .= AIOWPSecurity_Utility_Htaccess::$basic_htaccess_rules_marker_end . PHP_EOL; //Add feature marker end } return $rules; } public static function getrules_block_debug_log_access_htaccess() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_block_debug_log_file_access') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$debug_log_block_htaccess_rules_marker_start . PHP_EOL; //Add feature marker start $rules .= self::create_apache2_access_denied_rule('debug.log'); $rules .= AIOWPSecurity_Utility_Htaccess::$debug_log_block_htaccess_rules_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function will disable directory listings for all directories, add this line to the * site’s root .htaccess file. * NOTE: AllowOverride must be enabled in the httpd.conf file for this to work! */ public static function getrules_disable_index_views() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_disable_index_views') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$disable_index_views_marker_start . PHP_EOL; //Add feature marker start $rules .= 'Options -Indexes' . PHP_EOL; $rules .= AIOWPSecurity_Utility_Htaccess::$disable_index_views_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function will write rules to disable trace and track. * HTTP Trace attack (XST) can be used to return header requests * and grab cookies and other information and is used along with * a cross site scripting attacks (XSS) */ public static function getrules_disable_trace_and_track() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_disable_trace_and_track') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$disable_trace_track_marker_start . PHP_EOL; //Add feature marker start $rules .= '' . PHP_EOL; $rules .= 'RewriteEngine On' . PHP_EOL; $rules .= 'RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)' . PHP_EOL; $rules .= 'RewriteRule .* - [F]' . PHP_EOL; $rules .= '' . PHP_EOL; $rules .= AIOWPSecurity_Utility_Htaccess::$disable_trace_track_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function contains the rules for the 5G blacklist produced by Jeff Starr from perishablepress.com * NOTE: Since Jeff regularly updates and evolves his blacklist rules, ie, 5G->6G->7G.... we will update this function to reflect the latest blacklist release */ public static function getrules_5g_blacklist() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_enable_5g_firewall') == '1') { $rules .= AIOWPSecurity_Utility_Htaccess::$five_g_blacklist_marker_start . PHP_EOL; //Add feature marker start $rules .= '# 5G BLACKLIST/FIREWALL (2013) # @ http://perishablepress.com/5g-blacklist-2013/ # 5G:[QUERY STRINGS] RewriteEngine On RewriteBase / RewriteCond %{QUERY_STRING} (\"|%22).*(<|>|%3) [NC,OR] RewriteCond %{QUERY_STRING} (javascript:).*(\;) [NC,OR] RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3) [NC,OR] RewriteCond %{QUERY_STRING} (\\\|\.\./|`|=\'$|=%27$) [NC,OR] RewriteCond %{QUERY_STRING} (\;|\'|\"|%22).*(union|select|insert|drop|update|md5|benchmark|or|and|if) [NC,OR] RewriteCond %{QUERY_STRING} (base64_encode|localhost|mosconfig) [NC,OR] RewriteCond %{QUERY_STRING} (boot\.ini|echo.*kae|etc/passwd) [NC,OR] RewriteCond %{QUERY_STRING} (GLOBALS|REQUEST)(=|\[|%) [NC] RewriteRule .* - [F] # 5G:[USER AGENTS] # SetEnvIfNoCase User-Agent ^$ keep_out SetEnvIfNoCase User-Agent (binlar|casper|cmsworldmap|comodo|diavol|dotbot|feedfinder|flicky|ia_archiver|jakarta|kmccrew|nutch|planetwork|purebot|pycurl|skygrid|sucker|turnit|vikspider|zmeu) keep_out Order Allow,Deny Allow from all Deny from env=keep_out # 5G:[REQUEST STRINGS] RedirectMatch 403 (https?|ftp|php)\:// RedirectMatch 403 /(https?|ima|ucp)/ RedirectMatch 403 /(Permanent|Better)$ RedirectMatch 403 (\=\\\\\\\'|\=\\\%27|/\\\\\\\'/?|\)\.css\()$ RedirectMatch 403 (\,|\)\+|/\,/|\{0\}|\(/\(|\.\.\.|\+\+\+|\||\\\\\"\\\\\") RedirectMatch 403 \.(cgi|asp|aspx|cfg|dll|exe|jsp|mdb|sql|ini|rar)$ RedirectMatch 403 /(contac|fpw|install|pingserver|register)\.php$ RedirectMatch 403 (base64|crossdomain|localhost|wwwroot|e107\_) RedirectMatch 403 (eval\(|\_vti\_|\(null\)|echo.*kae|config\.xml) RedirectMatch 403 \.well\-known/host\-meta RedirectMatch 403 /function\.array\-rand RedirectMatch 403 \)\;\$\(this\)\.html\( RedirectMatch 403 proc/self/environ RedirectMatch 403 msnbot\.htm\)\.\_ RedirectMatch 403 /ref\.outcontrol RedirectMatch 403 com\_cropimage RedirectMatch 403 indonesia\.htm RedirectMatch 403 \{\$itemURL\} RedirectMatch 403 function\(\) RedirectMatch 403 labels\.rdf RedirectMatch 403 /playing.php RedirectMatch 403 muieblackcat # 5G:[REQUEST METHOD] RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) RewriteRule .* - [F] ' . PHP_EOL; $rules .= AIOWPSecurity_Utility_Htaccess::$five_g_blacklist_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function will write some directives to prevent image hotlinking */ public static function prevent_image_hotlinks() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_prevent_hotlinking') == '1') { $url_string = AIOWPSecurity_Utility_Htaccess::return_regularized_url(AIOWPSEC_WP_HOME_URL); if (false == $url_string) { $url_string = AIOWPSEC_WP_HOME_URL; } $rules .= AIOWPSecurity_Utility_Htaccess::$prevent_image_hotlinks_marker_start . PHP_EOL; //Add feature marker start $rules .= '' . PHP_EOL; $rules .= 'RewriteEngine On' . PHP_EOL; $rules .= 'RewriteCond %{HTTP_REFERER} !^$' . PHP_EOL; $rules .= 'RewriteCond %{REQUEST_FILENAME} -f' . PHP_EOL; $rules .= 'RewriteCond %{REQUEST_FILENAME} \.(gif|jpe?g?|png)$ [NC]' . PHP_EOL; $rules .= 'RewriteCond %{HTTP_REFERER} !^' . $url_string . ' [NC]' . PHP_EOL; $rules .= 'RewriteRule \.(gif|jpe?g?|png)$ - [F,NC,L]' . PHP_EOL; $rules .= '' . PHP_EOL; $rules .= AIOWPSecurity_Utility_Htaccess::$prevent_image_hotlinks_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function will write any custom htaccess rules into the server's .htaccess file * * @return string */ public static function getrules_custom_rules() { global $aio_wp_security; $rules = ''; if ($aio_wp_security->configs->get_value('aiowps_enable_custom_rules') == '1') { $custom_rules = $aio_wp_security->configs->get_value('aiowps_custom_rules'); $rules .= AIOWPSecurity_Utility_Htaccess::$custom_rules_marker_start . PHP_EOL; //Add feature marker start $rules .= $custom_rules . PHP_EOL; $rules .= AIOWPSecurity_Utility_Htaccess::$custom_rules_marker_end . PHP_EOL; //Add feature marker end } return $rules; } /** * This function will do a quick check to see if a file's contents are actually .htaccess specific. * At the moment it will look for the following tag somewhere in the file - "# BEGIN WordPress" * If it finds the tag it will deem the file as being .htaccess specific. * This was written to supplement the .htaccess restore functionality * * @param string $file_contents - the contents of the .htaccess file * * @return boolean */ public static function check_if_htaccess_contents($file_contents) { $is_htaccess = false; if (false === $file_contents || strlen($file_contents) == 0) { return -1; } if ((strpos($file_contents, '# BEGIN WordPress') !== false) || (strpos($file_contents, '# BEGIN') !== false)) { $is_htaccess = true; // It appears that we have some sort of .htaccess file } else { //see if we're at the end of the section $is_htaccess = false; } if ($is_htaccess) { return 1; } else { return -1; } } /** * This function will take a URL string and convert it to a form useful for using in htaccess rules. * Example: If URL passed to function = "http://www.mysite.com" * Result = "http(s)?://(.*)?mysite\.com" * * @param string $url * @return string */ public static function return_regularized_url($url) { if (filter_var($url, FILTER_VALIDATE_URL)) { $xyz = explode('.', $url); $y = ''; if (count($xyz) > 1) { $j = 1; foreach ($xyz as $x) { if (strpos($x, 'www') !== false) { $y .= str_replace('www', '(.*)?', $x); } elseif (1 == $j) { $y .= $x; } elseif ($j > 1) { $y .= '\.' . $x; } $j++; } //Now replace the "http" with "http(s)?" to cover both secure and non-secure if (strpos($y, 'https') !== false) { $y = str_replace('https', 'http(s)?', $y); } elseif (strpos($y, 'http') !== false) { $y = str_replace('http', 'http(s)?', $y); } return $y; } else { return $url; } } else { return false; } } /** * Returns a string with directive that contains rules * to effectively block access to any file that has basename matching * $filename under Apache webserver. * * @link http://httpd.apache.org/docs/current/mod/core.html#files * * @param string $filename * @return string */ protected static function create_apache2_access_denied_rule($filename) { return << Require all denied Order deny,allow Deny from all END; // Keep the empty line at the end of heredoc string, // otherwise the string will not end with end-of-line character! } /** * Convert an array of optionally asterisk-masked or partial IPv4 addresses * into network/netmask notation. Netmask value for a "full" IP is not * added (see example below) * * Example: * In: array('1.2.3.4', '5.6', '7.8.9.*') * Out: array('1.2.3.4', '5.6.0.0/16', '7.8.9.0/24') * * Simple validation is performed: * In: array('1.2.3.4.5', 'abc', '1.2.xyz.4') * Out: array() * * Simple sanitization is performed: * In: array('6.7.*.9') * Out: array('6.7.0.0/16') * * @param array $ips * @return array */ protected static function add_netmask($ips = array()) { $output = array(); if (empty($ips)) return array(); foreach ($ips as $ip) { //Check if ipv6 if (strpos($ip, ':') !== false) { //for now support whole ipv6 and CIDR range. $checked_ip = AIOWPSecurity_Utility_IP::is_ipv6_address_or_ipv6_range($ip); if (false != $checked_ip) { $output[] = $ip; } } $parts = explode('.', $ip); // Skip any IP that is empty, has more parts than expected or has // a non-numeric first part. if (empty($parts) || (count($parts) > 4) || !is_numeric($parts[0])) { continue; } $ip_out = array($parts[0]); $netmask = 8; for ($i = 1, $force_zero = false; ($i < 4) && $ip_out; $i++) { if ($force_zero || !isset($parts[$i]) || ('' === $parts[$i]) || ('*' === $parts[$i])) { $ip_out[$i] = '0'; $force_zero = true; // Forces all subsequent parts to be a zero } elseif (is_numeric($parts[$i])) { $ip_out[$i] = $parts[$i]; $netmask += 8; } else { // Invalid IP part detected, invalidate entire IP $ip_out = false; } } if ($ip_out) { // Glue IP back together, add netmask if IP denotes a subnet, store for output. $output[] = implode('.', $ip_out) . (($netmask < 32) ? ('/' . $netmask) : ''); } } return $output; } }