Plugin Directory

Changeset 3487781


Ignore:
Timestamp:
03/21/2026 01:25:07 PM (7 days ago)
Author:
Marc4
Message:

v2.0.1

Location:
security-hardener
Files:
7 added
3 edited

Legend:

Unmodified
Added
Removed
  • security-hardener/assets/blueprints/blueprint.json

    r3487446 r3487781  
    1616      "pluginZipFile": {
    1717        "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"
    1919      },
    2020      "options": {
  • security-hardener/trunk/readme.txt

    r3487446 r3487781  
    55Tested up to: 6.9
    66Requires PHP: 8.2
    7 Stable tag: 2.0.0
     7Stable tag: 2.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2323**XML-RPC Protection:**
    2424* Disable XML-RPC completely (enabled by default)
    25 * Remove pingback methods
     25* Remove pingback methods when XML-RPC is enabled
     26
     27**Pingback Protection:**
    2628* Disable self-pingbacks
     29* Remove X-Pingback header
     30* Block incoming pingbacks
    2731
    2832**User Enumeration Protection:**
     
    4650
    4751**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)
    5154* Security event logging system
    5255
     
    161164== Changelog ==
    162165
     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
    163180= 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"
    167190* Improved: "Clean wp_head" no longer removes feed links extra — avoids conflicts with plugins that rely on category and tag feeds
    168191* 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/
    172194
    173195= 1.0 - 2026-03-05 =
  • security-hardener/trunk/security-hardener.php

    r3487446 r3487781  
    44Plugin URI: https://wordpress.org/plugins/security-hardener/
    55Description: 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.0
     6Version: 2.0.1
    77Requires at least: 6.9
    88Tested up to: 6.9
     
    2020
    2121// Plugin constants
    22 define( 'WPSH_VERSION', '2.0.0' );
     22define( 'WPSH_VERSION', '2.0.1' );
    2323define( 'WPSH_FILE', __FILE__ );
    24 define( 'WPSH_DIR', plugin_dir_path( __FILE__ ) );
    25 define( 'WPSH_URL', plugin_dir_url( __FILE__ ) );
    2624define( 'WPSH_BASENAME', plugin_basename( __FILE__ ) );
    2725
     
    2927
    3028    /**
    31      * Main plugin class implementing WordPress.org hardening guidelines
     29     * Main plugin class applying WordPress security best practices
    3230     */
    3331    class WPHN_Hardener {
     
    8886            // Security headers
    8987            add_action( 'send_headers', array( $this, 'send_security_headers' ) );
    90 
    91             // Disable file editing
    92             // Already handled via constants
    9388
    9489            // XML-RPC hardening
     
    159154
    160155            // 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' ) );
    162157        }
    163158
     
    167162        public function deactivate(): void {
    168163            // 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' ) );
    170165        }
    171166
     
    173168         * Get default options
    174169         *
    175          * @return array
     170         * @return array<string, int>
    176171         */
    177172        private function get_default_options(): array {
     
    432427
    433428        /**
    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.
    435434         */
    436435        public function remove_login_hints(): void {
    437             // Remove "lost password" text that reveals if username exists
    438436            add_filter(
    439437                'login_messages',
    440438                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>';
    443444                    }
    444445                    return $message;
     
    532533         * Clear login attempts on successful login
    533534         *
    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 {
    538539            $ip           = $this->get_client_ip();
    539540            $attempts_key = 'wpsh_login_attempts_' . md5( $ip );
     
    594595         */
    595596        public function disable_self_pingbacks( &$links ): void {
    596             $home = get_option( 'home' );
     597            $home = home_url();
    597598            foreach ( $links as $l => $link ) {
    598                 if ( 0 === strpos( $link, $home ) ) {
     599                if ( str_starts_with( $link, $home ) ) {
    599600                    unset( $links[ $l ] );
    600601                }
     
    639640         * @param string $message Event message.
    640641         */
    641         private function log_security_event( $event_type, $message ): void {
     642        private function log_security_event( string $event_type, string $message ): void {
    642643            if ( ! $this->get_option( 'log_security_events', true ) ) {
    643644                return;
     
    652653            // Add new log entry
    653654            $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,
    659659            );
    660660
     
    742742
    743743            // 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            }
    751765
    752766            return $sanitized;
     
    943957                                    'block_user_enum',
    944958                                    __( 'Block user enumeration', 'security-hardener' ),
    945                                     __( 'Blocks ?author=N queries, secures REST 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' )
    946960                                );
    947961                                ?>
     
    963977                                $this->render_toggle_row(
    964978                                    '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' )
    966981                                );
    967982                                $this->render_number_row(
Note: See TracChangeset for help on using the changeset viewer.