Changeset 3487781
- Timestamp:
- 03/21/2026 01:25:07 PM (7 days ago)
- Location:
- security-hardener
- Files:
-
- 7 added
- 3 edited
-
assets/blueprints/blueprint.json (modified) (1 diff)
-
readme.txt (added)
-
security-hardener.php (added)
-
tags/2.0.1 (added)
-
tags/2.0.1/readme.txt (added)
-
tags/2.0.1/security-hardener.php (added)
-
tags/2.0.1/uninstall.php (added)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/security-hardener.php (modified) (15 diffs)
-
uninstall.php (added)
Legend:
- Unmodified
- Added
- Removed
-
security-hardener/assets/blueprints/blueprint.json
r3487446 r3487781 16 16 "pluginZipFile": { 17 17 "resource": "url", 18 "url": "https://downloads.wordpress.org/plugin/security-hardener.2.0. 0.zip"18 "url": "https://downloads.wordpress.org/plugin/security-hardener.2.0.1.zip" 19 19 }, 20 20 "options": { -
security-hardener/trunk/readme.txt
r3487446 r3487781 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.2 7 Stable tag: 2.0. 07 Stable tag: 2.0.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 23 23 **XML-RPC Protection:** 24 24 * Disable XML-RPC completely (enabled by default) 25 * Remove pingback methods 25 * Remove pingback methods when XML-RPC is enabled 26 27 **Pingback Protection:** 26 28 * Disable self-pingbacks 29 * Remove X-Pingback header 30 * Block incoming pingbacks 27 31 28 32 **User Enumeration Protection:** … … 46 50 47 51 **Additional Hardening:** 48 * Hide WordPress version (meta generator tag and asset query strings) (meta generator tag and asset query strings) 49 * Clean up `wp_head` output 50 * Remove unnecessary meta tags and links 52 * Hide WordPress version (meta generator tag and asset query strings) 53 * Remove obsolete wp_head items (RSD, WLW manifest, shortlink, emoji scripts) 51 54 * Security event logging system 52 55 … … 161 164 == Changelog == 162 165 166 = 2.0.1 - 2026-03-21 = 167 * Fixed: "Block user enumeration" description now mentions canonical redirect blocking 168 * Fixed: "Login rate limiting" toggle and number fields now have descriptions for consistency with all other options 169 * Fixed: Removed unused WPSH_DIR and WPSH_URL constants 170 * Fixed: Removed misleading inline comment about file editing in init() 171 * Fixed: readme — "Disable self-pingbacks" moved from XML-RPC section to its own Pingback Protection section 172 * Fixed: Unused parameters in clear_login_attempts() prefixed with underscore following WordPress Coding Standards 173 * Fixed: @return docblock for get_default_options() updated to array<string, int> for accuracy 174 * Fixed: Activation and deactivation log messages now pass through __() for translation 175 * Fixed: remove_login_hints() no longer relies on hardcoded English string comparison 176 * Fixed: strpos() replaced with str_starts_with() in disable_self_pingbacks() — consistent with PHP 8.2+ usage throughout the plugin 177 * Fixed: Explicit string types added to log_security_event() method signature 178 * Fixed: @param docblock of clear_login_attempts() updated to reflect renamed parameters $_user_login and $_user 179 163 180 = 2.0.0 - 2026-03-20 = 164 * Improved: Complete redesign of the settings page. 165 * Improved: Checkboxes replaced with CSS toggles. 166 * Improved: "Hide WordPress version" moved from "User Enumeration" card to "Other Settings". 181 * Improved: Complete redesign of the settings page — responsive 3-column card grid replaces the default WordPress Settings API table layout 182 * Improved: Checkboxes replaced with CSS toggles for clearer on/off state at a glance 183 * Improved: "Clear Logs" button moved inline next to the section heading 184 * Improved: Minimal inline CSS scoped to .wpsh-* classes — no external stylesheet enqueued 185 * Removed: "Enable security headers" master toggle — each header is now controlled individually 186 * Removed: Configurable HSTS max-age field — hardcoded to 31536000 seconds (1 year), the universally recommended value 187 * Fixed: HSTS "Include subdomains" now defaults to disabled — users must opt in explicitly 188 * Improved: "Hide WordPress version" now also strips the WordPress version from script and style asset URLs (?ver=), preventing version detection via asset URLs 189 * Improved: "Hide WordPress version" moved from "User Enumeration" to "Other Settings" 167 190 * Improved: "Clean wp_head" no longer removes feed links extra — avoids conflicts with plugins that rely on category and tag feeds 168 191 * Improved: "Clean wp_head" no longer removes wp_generator — already covered by "Hide WordPress version" 169 * Improved: All toggle descriptions updated for consistency. 170 * Added: Four missing recommendations from the official WordPress Hardening Guide. 171 * Removed: "Enable security headers" master toggle — each header is now controlled individually. 192 * Improved: All toggle descriptions updated for consistency — each option now explains what value or behaviour it applies 193 * Added: New "Additional Hardening Recommendations" section in the plugin admin page and readme, including four measures from the official WordPress Hardening Guide not previously listed: rename admin account, restrict database user privileges, protect wp-config.php, and block direct access to wp-includes/ 172 194 173 195 = 1.0 - 2026-03-05 = -
security-hardener/trunk/security-hardener.php
r3487446 r3487781 4 4 Plugin URI: https://wordpress.org/plugins/security-hardener/ 5 5 Description: Basic hardening: secure headers, disable XML-RPC/pingbacks, hide version, block user enumeration, generic login errors, and IP-based rate limiting. 6 Version: 2.0. 06 Version: 2.0.1 7 7 Requires at least: 6.9 8 8 Tested up to: 6.9 … … 20 20 21 21 // Plugin constants 22 define( 'WPSH_VERSION', '2.0. 0' );22 define( 'WPSH_VERSION', '2.0.1' ); 23 23 define( 'WPSH_FILE', __FILE__ ); 24 define( 'WPSH_DIR', plugin_dir_path( __FILE__ ) );25 define( 'WPSH_URL', plugin_dir_url( __FILE__ ) );26 24 define( 'WPSH_BASENAME', plugin_basename( __FILE__ ) ); 27 25 … … 29 27 30 28 /** 31 * Main plugin class implementing WordPress.org hardening guidelines29 * Main plugin class applying WordPress security best practices 32 30 */ 33 31 class WPHN_Hardener { … … 88 86 // Security headers 89 87 add_action( 'send_headers', array( $this, 'send_security_headers' ) ); 90 91 // Disable file editing92 // Already handled via constants93 88 94 89 // XML-RPC hardening … … 159 154 160 155 // Log activation 161 $this->log_security_event( 'plugin_activated', 'Security Hardener plugin activated');156 $this->log_security_event( 'plugin_activated', __( 'Security Hardener plugin activated', 'security-hardener' ) ); 162 157 } 163 158 … … 167 162 public function deactivate(): void { 168 163 // Log deactivation 169 $this->log_security_event( 'plugin_deactivated', 'Security Hardener plugin deactivated');164 $this->log_security_event( 'plugin_deactivated', __( 'Security Hardener plugin deactivated', 'security-hardener' ) ); 170 165 } 171 166 … … 173 168 * Get default options 174 169 * 175 * @return array 170 * @return array<string, int> 176 171 */ 177 172 private function get_default_options(): array { … … 432 427 433 428 /** 434 * Remove login hints from login page 429 * Remove login hints from login page. 430 * 431 * Replaces the lost-password confirmation message with a generic one to avoid 432 * revealing whether a username/email exists in the database. Only intercepts 433 * messages during the lostpassword flow, leaving other login messages intact. 435 434 */ 436 435 public function remove_login_hints(): void { 437 // Remove "lost password" text that reveals if username exists438 436 add_filter( 439 437 'login_messages', 440 438 function ( $message ) { 441 if ( strpos( $message, 'check your email' ) !== false ) { 442 return '<strong>' . esc_html__( 'Check your email for the confirmation link.', 'security-hardener' ) . '</strong>'; 439 // Only replace messages during the lost password flow. 440 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check on action param 441 $action = isset( $_REQUEST['action'] ) ? sanitize_key( $_REQUEST['action'] ) : ''; 442 if ( in_array( $action, [ 'lostpassword', 'retrievepassword' ], true ) && ! empty( $message ) ) { 443 return '<p class="message">' . esc_html__( 'Check your email for the confirmation link.', 'security-hardener' ) . '</p>'; 443 444 } 444 445 return $message; … … 532 533 * Clear login attempts on successful login 533 534 * 534 * @param string $ user_login Username.535 * @param WP_User $ user User object.536 */ 537 public function clear_login_attempts( $ user_login, $user ): void {535 * @param string $_user_login Username (unused, required by hook). 536 * @param WP_User $_user User object (unused, required by hook). 537 */ 538 public function clear_login_attempts( $_user_login, $_user ): void { 538 539 $ip = $this->get_client_ip(); 539 540 $attempts_key = 'wpsh_login_attempts_' . md5( $ip ); … … 594 595 */ 595 596 public function disable_self_pingbacks( &$links ): void { 596 $home = get_option( 'home');597 $home = home_url(); 597 598 foreach ( $links as $l => $link ) { 598 if ( 0 === strpos( $link, $home ) ) {599 if ( str_starts_with( $link, $home ) ) { 599 600 unset( $links[ $l ] ); 600 601 } … … 639 640 * @param string $message Event message. 640 641 */ 641 private function log_security_event( $event_type,$message ): void {642 private function log_security_event( string $event_type, string $message ): void { 642 643 if ( ! $this->get_option( 'log_security_events', true ) ) { 643 644 return; … … 652 653 // Add new log entry 653 654 $logs[] = array( 654 'timestamp' => current_time( 'mysql' ), 655 'type' => $event_type, 656 'message' => $message, 657 'ip' => $ip, 658 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', 655 'timestamp' => current_time( 'mysql' ), 656 'type' => $event_type, 657 'message' => $message, 658 'ip' => $ip, 659 659 ); 660 660 … … 742 742 743 743 // Numeric fields 744 $sanitized['rate_limit_attempts'] = isset( $input['rate_limit_attempts'] ) 745 ? max( 3, min( 20, absint( $input['rate_limit_attempts'] ) ) ) 746 : 5; 747 748 $sanitized['rate_limit_minutes'] = isset( $input['rate_limit_minutes'] ) 749 ? max( 5, min( 1440, absint( $input['rate_limit_minutes'] ) ) ) 750 : 15; 744 $raw_attempts = isset( $input['rate_limit_attempts'] ) ? absint( $input['rate_limit_attempts'] ) : 5; 745 $sanitized['rate_limit_attempts'] = max( 3, min( 20, $raw_attempts ) ); 746 if ( $raw_attempts !== $sanitized['rate_limit_attempts'] ) { 747 add_settings_error( 748 self::OPTION_NAME, 749 'rate_limit_attempts_range', 750 __( '"Failed attempts before block" must be between 3 and 20. Value has been adjusted.', 'security-hardener' ), 751 'warning' 752 ); 753 } 754 755 $raw_minutes = isset( $input['rate_limit_minutes'] ) ? absint( $input['rate_limit_minutes'] ) : 15; 756 $sanitized['rate_limit_minutes'] = max( 5, min( 1440, $raw_minutes ) ); 757 if ( $raw_minutes !== $sanitized['rate_limit_minutes'] ) { 758 add_settings_error( 759 self::OPTION_NAME, 760 'rate_limit_minutes_range', 761 __( '"Block duration" must be between 5 and 1440 minutes. Value has been adjusted.', 'security-hardener' ), 762 'warning' 763 ); 764 } 751 765 752 766 return $sanitized; … … 943 957 'block_user_enum', 944 958 __( 'Block user enumeration', 'security-hardener' ), 945 __( 'Blocks ?author=N queries, securesREST API user endpoints, and removes users from sitemaps.', 'security-hardener' )959 __( 'Blocks ?author=N queries, canonical redirects, REST API user endpoints, and removes users from sitemaps.', 'security-hardener' ) 946 960 ); 947 961 ?> … … 963 977 $this->render_toggle_row( 964 978 'rate_limit_login', 965 __( 'Login rate limiting', 'security-hardener' ) 979 __( 'Login rate limiting', 'security-hardener' ), 980 __( 'Blocks an IP after repeated failed login attempts.', 'security-hardener' ) 966 981 ); 967 982 $this->render_number_row(
Note: See TracChangeset
for help on using the changeset viewer.