Plugin Directory

Changeset 3487446


Ignore:
Timestamp:
03/20/2026 07:58:57 PM (8 days ago)
Author:
Marc4
Message:

v2.0.0

Location:
security-hardener
Files:
4 added
3 edited

Legend:

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

    r3475624 r3487446  
    1616      "pluginZipFile": {
    1717        "resource": "url",
    18         "url": "https://downloads.wordpress.org/plugin/security-hardener.1.0.zip"
     18        "url": "https://downloads.wordpress.org/plugin/security-hardener.2.0.0.zip"
    1919      },
    2020      "options": {
  • security-hardener/trunk/readme.txt

    r3475624 r3487446  
    55Tested up to: 6.9
    66Requires PHP: 8.2
    7 Stable tag: 1.0
     7Stable tag: 2.0.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1313== Description ==
    1414
    15 **Security Hardener** implements the official WordPress hardening guidelines from the [WordPress Advanced Administration / Security / Hardening](https://developer.wordpress.org/advanced-administration/security/hardening/) documentation. It uses WordPress core functions and follows best practices without modifying core files.
     15**Security Hardener** applies WordPress security best practices based on the [WordPress Advanced Administration / Security / Hardening](https://developer.wordpress.org/advanced-administration/security/hardening/) documentation and widely accepted hardening measures. It uses WordPress core functions and follows best practices without modifying core files.
    1616
    1717= Key Features =
     
    4343* `Referrer-Policy: strict-origin-when-cross-origin`
    4444* `Permissions-Policy` (restricts geolocation, microphone, camera)
    45 * Optional HSTS (HTTP Strict Transport Security) for HTTPS sites
     45* Optional HSTS (HTTP Strict Transport Security) for HTTPS sites — max-age set to 1 year
    4646
    4747**Additional Hardening:**
    48 * Hide WordPress version
     48* Hide WordPress version (meta generator tag and asset query strings) (meta generator tag and asset query strings)
    4949* Clean up `wp_head` output
    5050* Remove unnecessary meta tags and links
     
    5353> ⚠️ **Important:** Always test security settings in a staging environment first. Some features may affect third-party integrations or plugins.
    5454
    55 **Privacy:** This plugin does not send data to external services and does not create custom database tables. It stores plugin settings and a security event log in the WordPress options table, and uses transients for temporary login attempt tracking. All data is deleted on uninstall.
     55**Privacy:** This plugin does not send data to external services and does not create custom database tables. It stores plugin settings and a security event log in the WordPress options table, and uses transients for temporary login attempt tracking. All data is preserved on uninstall by default and only deleted if the "Delete all data on uninstall" option is explicitly enabled.
    5656
    5757== Installation ==
     
    7575* Login rate limiting (5 attempts per 15 minutes)
    7676* Security headers
    77 * WordPress version hiding
     77* WordPress version hiding (meta generator tag and asset query strings)
    7878* Clean wp_head output
    7979* Security event logging
     
    105105
    106106When HSTS is enabled (HTTPS only):
    107 * `Strict-Transport-Security: max-age=31536000; includeSubDomains` (configurable)
     107* `Strict-Transport-Security: max-age=31536000` (optionally with `includeSubDomains` if enabled)
    108108
    109109= Does the plugin work with page caching? =
     
    160160
    161161== Changelog ==
     162
     163= 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".
     167* Improved: "Clean wp_head" no longer removes feed links extra — avoids conflicts with plugins that rely on category and tag feeds
     168* 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.
    162172
    163173= 1.0 - 2026-03-05 =
  • security-hardener/trunk/security-hardener.php

    r3475624 r3487446  
    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: 1.0
     6Version: 2.0.0
    77Requires at least: 6.9
    88Tested up to: 6.9
     
    2020
    2121// Plugin constants
    22 define( 'WPSH_VERSION', '1.0' );
     22define( 'WPSH_VERSION', '2.0.0' );
    2323define( 'WPSH_FILE', __FILE__ );
    2424define( 'WPSH_DIR', plugin_dir_path( __FILE__ ) );
     
    102102                add_filter( 'the_generator', '__return_empty_string' );
    103103                remove_action( 'wp_head', 'wp_generator' );
     104                add_filter( 'script_loader_src', array( $this, 'remove_wp_version_from_assets' ) );
     105                add_filter( 'style_loader_src', array( $this, 'remove_wp_version_from_assets' ) );
    104106            }
    105107
     
    201203
    202204                // Security headers
    203                 'enable_headers'           => 1,
    204205                'header_x_frame'           => 1,
    205206                'header_x_content'         => 1,
     
    209210                // HTTPS
    210211                'enable_hsts'              => 0, // Off by default - requires HTTPS
    211                 'hsts_max_age'             => 31536000,
    212                 'hsts_subdomains'          => 1,
     212                'hsts_subdomains'          => 0,
    213213                'hsts_preload'             => 0,
    214214
     
    263263         */
    264264        public function send_security_headers(): void {
    265             if ( ! $this->get_option( 'enable_headers', true ) ) {
    266                 return;
    267             }
    268 
    269265            // Prevent sent headers warning
    270266            if ( headers_sent() ) {
     
    294290            // HSTS (only if HTTPS and enabled)
    295291            if ( $this->get_option( 'enable_hsts', false ) && is_ssl() ) {
    296                 $max_age     = absint( $this->get_option( 'hsts_max_age', 31536000 ) );
    297                 $hsts_header = "Strict-Transport-Security: max-age={$max_age}";
    298 
    299                 if ( $this->get_option( 'hsts_subdomains', true ) ) {
     292                $hsts_header = 'Strict-Transport-Security: max-age=31536000';
     293
     294                if ( $this->get_option( 'hsts_subdomains', false ) ) {
    300295                    $hsts_header .= '; includeSubDomains';
    301296                }
     
    319314            unset( $methods['pingback.extensions.getPingbacks'] );
    320315            return $methods;
     316        }
     317
     318        /**
     319         * Remove WordPress version number from script and style URLs.
     320         *
     321         * Only strips ?ver= when its value matches the WordPress core version,
     322         * leaving plugin and theme asset versions intact.
     323         *
     324         * @param string $src Asset URL.
     325         * @return string
     326         */
     327        public function remove_wp_version_from_assets( string $src ): string {
     328            global $wp_version;
     329            if ( str_contains( $src, "ver={$wp_version}" ) ) {
     330                $src = remove_query_arg( 'ver', $src );
     331            }
     332            return $src;
    321333        }
    322334
     
    611623            remove_action( 'wp_head', 'wlwmanifest_link' );
    612624
    613             // Remove WordPress version
    614             remove_action( 'wp_head', 'wp_generator' );
    615 
    616625            // Remove shortlink
    617626            remove_action( 'wp_head', 'wp_shortlink_wp_head' );
    618 
    619             // Remove feed links (keep main feed)
    620             remove_action( 'wp_head', 'feed_links_extra', 3 );
    621627
    622628            // Remove emoji scripts
     
    677683        /**
    678684         * Register settings
     685         *
     686         * Only register_setting() is needed here — sanitize_callback handles validation,
     687         * and settings_fields() in the form generates the nonce. Sections and fields are
     688         * rendered manually in render_settings_page() via the custom card grid.
    679689         */
    680690        public function register_settings(): void {
     
    686696                    'sanitize_callback' => array( $this, 'sanitize_options' ),
    687697                )
    688             );
    689 
    690             // File Editing section
    691             add_settings_section(
    692                 'wpsh_file_editing',
    693                 __( 'File Editing', 'security-hardener' ),
    694                 function () {
    695                     echo '<p>' . esc_html__( 'Control file editing capabilities in WordPress admin.', 'security-hardener' ) . '</p>';
    696                 },
    697                 'security-hardener'
    698             );
    699 
    700             $this->add_checkbox_field( 'disable_file_edit', __( 'Disable file editor', 'security-hardener' ), 'wpsh_file_editing', __( 'Prevents editing of theme and plugin files through WordPress admin.', 'security-hardener' ) );
    701             $this->add_checkbox_field( 'disable_file_mods', __( 'Disable all file modifications', 'security-hardener' ), 'wpsh_file_editing', '<strong>' . esc_html__( 'Warning:', 'security-hardener' ) . '</strong> ' . esc_html__( 'This will disable plugin/theme updates and installations.', 'security-hardener' ) );
    702 
    703             // XML-RPC section
    704             add_settings_section(
    705                 'wpsh_xmlrpc',
    706                 __( 'XML-RPC', 'security-hardener' ),
    707                 function () {
    708                     echo '<p>' . esc_html__( 'XML-RPC is often targeted by attackers. Disable unless you need it for Jetpack or mobile apps.', 'security-hardener' ) . '</p>';
    709                 },
    710                 'security-hardener'
    711             );
    712 
    713             $this->add_checkbox_field( 'disable_xmlrpc', __( 'Disable XML-RPC', 'security-hardener' ), 'wpsh_xmlrpc' );
    714             $this->add_checkbox_field( 'disable_pingbacks', __( 'Disable pingbacks', 'security-hardener' ), 'wpsh_xmlrpc' );
    715 
    716             // User Enumeration section
    717             add_settings_section(
    718                 'wpsh_user_enum',
    719                 __( 'User Enumeration Protection', 'security-hardener' ),
    720                 function () {
    721                     echo '<p>' . esc_html__( 'Prevent attackers from discovering usernames through various WordPress features.', 'security-hardener' ) . '</p>';
    722                 },
    723                 'security-hardener'
    724             );
    725 
    726             $this->add_checkbox_field( 'block_user_enum', __( 'Block user enumeration', 'security-hardener' ), 'wpsh_user_enum', __( 'Blocks ?author=N queries, secures REST API user endpoints, and removes users from sitemaps.', 'security-hardener' ) );
    727             $this->add_checkbox_field( 'hide_wp_version', __( 'Hide WordPress version', 'security-hardener' ), 'wpsh_user_enum' );
    728 
    729             // Login Security section
    730             add_settings_section(
    731                 'wpsh_login',
    732                 __( 'Login Security', 'security-hardener' ),
    733                 function () {
    734                     echo '<p>' . esc_html__( 'Protect against brute force attacks and information disclosure.', 'security-hardener' ) . '</p>';
    735                 },
    736                 'security-hardener'
    737             );
    738 
    739             $this->add_checkbox_field( 'secure_login', __( 'Generic login errors', 'security-hardener' ), 'wpsh_login', __( 'Don\'t reveal whether username or password was incorrect.', 'security-hardener' ) );
    740             $this->add_checkbox_field( 'rate_limit_login', __( 'Enable login rate limiting', 'security-hardener' ), 'wpsh_login' );
    741 
    742             add_settings_field(
    743                 'rate_limit_attempts',
    744                 __( 'Failed attempts before block', 'security-hardener' ),
    745                 array( $this, 'render_number_field' ),
    746                 'security-hardener',
    747                 'wpsh_login',
    748                 array(
    749                     'field_id' => 'rate_limit_attempts',
    750                     'min'      => 3,
    751                     'max'      => 20,
    752                     'default'  => 5,
    753                 )
    754             );
    755 
    756             add_settings_field(
    757                 'rate_limit_minutes',
    758                 __( 'Block duration (minutes)', 'security-hardener' ),
    759                 array( $this, 'render_number_field' ),
    760                 'security-hardener',
    761                 'wpsh_login',
    762                 array(
    763                     'field_id' => 'rate_limit_minutes',
    764                     'min'      => 5,
    765                     'max'      => 1440,
    766                     'default'  => 15,
    767                 )
    768             );
    769 
    770             // Security Headers section
    771             add_settings_section(
    772                 'wpsh_headers',
    773                 __( 'Security Headers', 'security-hardener' ),
    774                 function () {
    775                     echo '<p>' . esc_html__( 'Send HTTP security headers to protect against various attacks.', 'security-hardener' ) . '</p>';
    776                 },
    777                 'security-hardener'
    778             );
    779 
    780             $this->add_checkbox_field( 'enable_headers', __( 'Enable security headers', 'security-hardener' ), 'wpsh_headers' );
    781             $this->add_checkbox_field( 'header_x_frame', __( 'X-Frame-Options (clickjacking protection)', 'security-hardener' ), 'wpsh_headers' );
    782             $this->add_checkbox_field( 'header_x_content', __( 'X-Content-Type-Options (MIME sniffing protection)', 'security-hardener' ), 'wpsh_headers' );
    783             $this->add_checkbox_field( 'header_referrer', __( 'Referrer-Policy', 'security-hardener' ), 'wpsh_headers' );
    784             $this->add_checkbox_field( 'header_permissions', __( 'Permissions-Policy', 'security-hardener' ), 'wpsh_headers' );
    785 
    786             // HSTS section
    787             add_settings_section(
    788                 'wpsh_hsts',
    789                 __( 'HSTS (HTTPS Sites Only)', 'security-hardener' ),
    790                 function () {
    791                     echo '<p>' . esc_html__( 'HTTP Strict Transport Security forces HTTPS. Only enable if your entire site uses HTTPS.', 'security-hardener' ) . '</p>';
    792                     if ( ! is_ssl() ) {
    793                         echo '<p class="description" style="color: #d63638;"><strong>' . esc_html__( 'Warning: Your site is not currently using HTTPS. Do not enable HSTS.', 'security-hardener' ) . '</strong></p>';
    794                     }
    795                 },
    796                 'security-hardener'
    797             );
    798 
    799             $this->add_checkbox_field( 'enable_hsts', __( 'Enable HSTS', 'security-hardener' ), 'wpsh_hsts', '<strong>' . esc_html__( 'Warning:', 'security-hardener' ) . '</strong> ' . esc_html__( 'Only enable if your site fully supports HTTPS.', 'security-hardener' ) );
    800             $this->add_checkbox_field( 'hsts_subdomains', __( 'Include subdomains', 'security-hardener' ), 'wpsh_hsts' );
    801             $this->add_checkbox_field( 'hsts_preload', __( 'Enable preload', 'security-hardener' ), 'wpsh_hsts', __( 'Submit to <a href="https://hstspreload.org/" target="_blank">HSTS Preload List</a> (requires 1 year max-age).', 'security-hardener' ) );
    802 
    803             // Other section
    804             add_settings_section(
    805                 'wpsh_other',
    806                 __( 'Other Settings', 'security-hardener' ),
    807                 null,
    808                 'security-hardener'
    809             );
    810 
    811             $this->add_checkbox_field( 'clean_head', __( 'Clean wp_head', 'security-hardener' ), 'wpsh_other', __( 'Remove unnecessary items from &lt;head&gt; section.', 'security-hardener' ) );
    812             $this->add_checkbox_field( 'log_security_events', __( 'Log security events', 'security-hardener' ), 'wpsh_other', __( 'Keep a log of security events (last 100 entries).', 'security-hardener' ) );
    813             $this->add_checkbox_field( 'delete_data_on_uninstall', __( 'Delete all data on uninstall', 'security-hardener' ), 'wpsh_other', '<strong>' . esc_html__( 'Warning:', 'security-hardener' ) . '</strong> ' . esc_html__( 'When enabled, all plugin settings and security logs will be permanently deleted when the plugin is uninstalled. Disabled by default to preserve data.', 'security-hardener' ) );
    814         }
    815 
    816         /**
    817          * Add checkbox field helper
    818          *
    819          * @param string $field_id Field ID.
    820          * @param string $label Field label.
    821          * @param string $section Section ID.
    822          * @param string $description Optional description.
    823          */
    824         private function add_checkbox_field( $field_id, $label, $section, $description = '' ): void {
    825             add_settings_field(
    826                 $field_id,
    827                 $label,
    828                 array( $this, 'render_checkbox_field' ),
    829                 'security-hardener',
    830                 $section,
    831                 array(
    832                     'field_id'    => $field_id,
    833                     'description' => $description,
    834                 )
    835             );
    836         }
    837 
    838         /**
    839          * Render checkbox field
    840          *
    841          * @param array $args {
    842          *     Field arguments.
    843          *
    844          *     @type string $field_id    Option key in the stored options array.
    845          *     @type string $description Optional description shown beside the checkbox.
    846          * }
    847          */
    848         public function render_checkbox_field( $args ): void {
    849             $field_id = $args['field_id'];
    850             $value    = $this->get_option( $field_id, 0 );
    851             $checked  = checked( 1, $value, false );
    852 
    853             printf(
    854                 '<label><input type="checkbox" name="%s[%s]" value="1" %s /> %s</label>',
    855                 esc_attr( self::OPTION_NAME ),
    856                 esc_attr( $field_id ),
    857                 $checked, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    858                 ! empty( $args['description'] ) ? wp_kses_post( $args['description'] ) : esc_html__( 'Enable', 'security-hardener' )
    859             );
    860         }
    861 
    862         /**
    863          * Render number field
    864          *
    865          * @param array $args Field arguments.
    866          */
    867         public function render_number_field( $args ): void {
    868             $field_id = $args['field_id'];
    869             $value    = $this->get_option( $field_id, $args['default'] );
    870             $min      = isset( $args['min'] ) ? absint( $args['min'] ) : 1;
    871             $max      = isset( $args['max'] ) ? absint( $args['max'] ) : 999;
    872 
    873             printf(
    874                 '<input type="number" name="%s[%s]" value="%s" min="%d" max="%d" class="small-text" />',
    875                 esc_attr( self::OPTION_NAME ),
    876                 esc_attr( $field_id ),
    877                 esc_attr( $value ),
    878                 absint( $min ),
    879                 absint( $max )
    880698            );
    881699        }
     
    904722                'secure_login',
    905723                'rate_limit_login',
    906                 'enable_headers',
    907724                'header_x_frame',
    908725                'header_x_content',
     
    933750                : 15;
    934751
    935             $sanitized['hsts_max_age'] = isset( $input['hsts_max_age'] )
    936                 ? max( 300, min( 63072000, absint( $input['hsts_max_age'] ) ) )
    937                 : 31536000;
    938 
    939752            return $sanitized;
     753        }
     754
     755        /**
     756         * Output inline admin styles for the settings page grid layout and toggles.
     757         * Uses only WordPress admin colour variables so it adapts to any admin theme.
     758         */
     759        private function render_admin_styles(): void {
     760            ?>
     761            <style>
     762                .wpsh-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:16px;}
     763                .wpsh-card{background:#fff;border:1px solid #c3c4c7;border-radius:4px;padding:0;}
     764                .wpsh-card-header{padding:12px 16px;border-bottom:1px solid #c3c4c7;}
     765                .wpsh-card-title{margin:0;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#646970;}
     766                .wpsh-card-body{padding:4px 0;}
     767                .wpsh-row{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:10px 16px;border-bottom:1px solid #f0f0f1;}
     768                .wpsh-row:last-child{border-bottom:none;}
     769                .wpsh-row-text{flex:1;min-width:0;}
     770                .wpsh-row-label{font-size:13px;color:#1d2327;line-height:1.4;}
     771                .wpsh-row-desc{font-size:12px;color:#646970;margin-top:2px;line-height:1.4;}
     772                .wpsh-row-number{display:flex;align-items:center;gap:6px;flex-shrink:0;}
     773                .wpsh-row-number input{width:60px;}
     774                .wpsh-row-number span{font-size:12px;color:#646970;}
     775                .wpsh-toggle{position:relative;display:inline-block;width:36px;height:20px;flex-shrink:0;margin-top:1px;}
     776                .wpsh-toggle input{opacity:0;width:0;height:0;position:absolute;}
     777                .wpsh-toggle-track{position:absolute;inset:0;background:#c3c4c7;border-radius:20px;transition:background .15s;}
     778                .wpsh-toggle input:checked~.wpsh-toggle-track{background:#2271b1;}
     779                .wpsh-toggle-thumb{position:absolute;width:14px;height:14px;background:#fff;border-radius:50%;top:3px;left:3px;transition:left .15s;pointer-events:none;}
     780                .wpsh-toggle input:checked~.wpsh-toggle-thumb{left:19px;}
     781                .wpsh-toggle input:focus~.wpsh-toggle-track{box-shadow:0 0 0 2px #2271b1,0 0 0 4px rgba(34,113,177,.3);}
     782                .wpsh-badge{display:inline-block;font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;margin-left:5px;vertical-align:middle;text-transform:uppercase;letter-spacing:.03em;}
     783                .wpsh-badge-warn{background:#fcf9e8;color:#996800;border:1px solid #f0c33c;}
     784                .wpsh-badge-https{background:#f0f6fc;color:#2271b1;border:1px solid #72aee6;}
     785                .wpsh-hsts-warn{font-size:12px;color:#d63638;padding:8px 16px 0;font-weight:600;}
     786                .wpsh-section-header{display:flex;align-items:center;justify-content:space-between;padding:20px 0 8px;}
     787                .wpsh-logs-table{margin-top:8px;}
     788                .wpsh-recommendations{margin-top:0;}
     789                .wpsh-recommendations li{margin-bottom:4px;}
     790                .wpsh-save-bar{margin:20px 0 4px;}
     791                @media(max-width:1200px){.wpsh-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
     792                @media(max-width:782px){.wpsh-grid{grid-template-columns:minmax(0,1fr);}}
     793            </style>
     794            <?php
     795        }
     796
     797        /**
     798         * Render a single card row with a toggle.
     799         *
     800         * @param string $field_id    Option key.
     801         * @param string $label       Row label.
     802         * @param string $description Optional description.
     803         * @param string $badge_html  Optional badge HTML (already escaped).
     804         */
     805        private function render_toggle_row( string $field_id, string $label, string $description = '', string $badge_html = '' ): void {
     806            $value      = $this->get_option( $field_id, 0 );
     807            $checked    = checked( 1, $value, false );
     808            $input_name = esc_attr( self::OPTION_NAME ) . '[' . esc_attr( $field_id ) . ']';
     809            ?>
     810            <div class="wpsh-row">
     811                <div class="wpsh-row-text">
     812                    <div class="wpsh-row-label">
     813                        <?php echo esc_html( $label ); ?>
     814                        <?php echo $badge_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped by caller ?>
     815                    </div>
     816                    <?php if ( $description ) : ?>
     817                        <div class="wpsh-row-desc"><?php echo wp_kses_post( $description ); ?></div>
     818                    <?php endif; ?>
     819                </div>
     820                <label class="wpsh-toggle">
     821                    <input type="checkbox" name="<?php echo $input_name; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fully escaped above ?>" value="1" <?php echo $checked; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output of checked() ?>>
     822                    <div class="wpsh-toggle-track"></div>
     823                    <div class="wpsh-toggle-thumb"></div>
     824                </label>
     825            </div>
     826            <?php
     827        }
     828
     829        /**
     830         * Render a single card row with a number input.
     831         *
     832         * @param string $field_id Field ID.
     833         * @param string $label    Row label.
     834         * @param int    $min      Minimum value.
     835         * @param int    $max      Maximum value.
     836         * @param int    $default  Default value.
     837         * @param string $unit     Unit label shown after the input.
     838         */
     839        private function render_number_row( string $field_id, string $label, int $min, int $max, int $default, string $unit = '' ): void {
     840            $value      = $this->get_option( $field_id, $default );
     841            $input_name = esc_attr( self::OPTION_NAME ) . '[' . esc_attr( $field_id ) . ']';
     842            ?>
     843            <div class="wpsh-row">
     844                <div class="wpsh-row-text">
     845                    <div class="wpsh-row-label"><?php echo esc_html( $label ); ?></div>
     846                </div>
     847                <div class="wpsh-row-number">
     848                    <input type="number"
     849                        name="<?php echo $input_name; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- fully escaped above ?>"
     850                        value="<?php echo esc_attr( $value ); ?>"
     851                        min="<?php echo absint( $min ); ?>"
     852                        max="<?php echo absint( $max ); ?>"
     853                        class="small-text" />
     854                    <?php if ( $unit ) : ?>
     855                        <span><?php echo esc_html( $unit ); ?></span>
     856                    <?php endif; ?>
     857                </div>
     858            </div>
     859            <?php
    940860        }
    941861
     
    948868            }
    949869
     870            $this->render_admin_styles();
     871
     872            $warn_badge  = '<span class="wpsh-badge wpsh-badge-warn">' . esc_html__( 'Caution', 'security-hardener' ) . '</span>';
     873            $https_badge = '<span class="wpsh-badge wpsh-badge-https">' . esc_html__( 'HTTPS only', 'security-hardener' ) . '</span>';
    950874            ?>
    951875            <div class="wrap">
     
    954878                <?php settings_errors(); ?>
    955879
    956                 <div class="notice notice-info">
     880                <div class="notice notice-info inline" style="margin-top:12px;">
    957881                    <p>
    958882                        <strong><?php esc_html_e( 'Important:', 'security-hardener' ); ?></strong>
     
    962886
    963887                <form method="post" action="options.php">
    964                     <?php
    965                     settings_fields( 'wpsh_settings' );
    966                     do_settings_sections( 'security-hardener' );
    967                     submit_button();
    968                     ?>
     888                    <?php settings_fields( 'wpsh_settings' ); ?>
     889
     890                    <div class="wpsh-grid">
     891
     892                        <!-- File Editing -->
     893                        <div class="wpsh-card">
     894                            <div class="wpsh-card-header">
     895                                <h2 class="wpsh-card-title"><?php esc_html_e( 'File editing', 'security-hardener' ); ?></h2>
     896                            </div>
     897                            <div class="wpsh-card-body">
     898                                <?php
     899                                $this->render_toggle_row(
     900                                    'disable_file_edit',
     901                                    __( 'Disable file editor', 'security-hardener' ),
     902                                    __( 'Prevents editing theme and plugin files in WordPress admin.', 'security-hardener' )
     903                                );
     904                                $this->render_toggle_row(
     905                                    'disable_file_mods',
     906                                    __( 'Disable all file modifications', 'security-hardener' ),
     907                                    __( 'Blocks plugin/theme updates and installations.', 'security-hardener' ),
     908                                    $warn_badge
     909                                );
     910                                ?>
     911                            </div>
     912                        </div>
     913
     914                        <!-- XML-RPC & Pingbacks -->
     915                        <div class="wpsh-card">
     916                            <div class="wpsh-card-header">
     917                                <h2 class="wpsh-card-title"><?php esc_html_e( 'XML-RPC &amp; pingbacks', 'security-hardener' ); ?></h2>
     918                            </div>
     919                            <div class="wpsh-card-body">
     920                                <?php
     921                                $this->render_toggle_row(
     922                                    'disable_xmlrpc',
     923                                    __( 'Disable XML-RPC', 'security-hardener' ),
     924                                    __( 'Recommended unless you use Jetpack or the mobile app.', 'security-hardener' )
     925                                );
     926                                $this->render_toggle_row(
     927                                    'disable_pingbacks',
     928                                    __( 'Disable pingbacks', 'security-hardener' ),
     929                                    __( 'Removes the X-Pingback header and disables incoming and self-referencing pingbacks.', 'security-hardener' )
     930                                );
     931                                ?>
     932                            </div>
     933                        </div>
     934
     935                        <!-- User Enumeration -->
     936                        <div class="wpsh-card">
     937                            <div class="wpsh-card-header">
     938                                <h2 class="wpsh-card-title"><?php esc_html_e( 'User enumeration', 'security-hardener' ); ?></h2>
     939                            </div>
     940                            <div class="wpsh-card-body">
     941                                <?php
     942                                $this->render_toggle_row(
     943                                    'block_user_enum',
     944                                    __( 'Block user enumeration', 'security-hardener' ),
     945                                    __( 'Blocks ?author=N queries, secures REST API user endpoints, and removes users from sitemaps.', 'security-hardener' )
     946                                );
     947                                ?>
     948                            </div>
     949                        </div>
     950
     951                        <!-- Login Security -->
     952                        <div class="wpsh-card">
     953                            <div class="wpsh-card-header">
     954                                <h2 class="wpsh-card-title"><?php esc_html_e( 'Login security', 'security-hardener' ); ?></h2>
     955                            </div>
     956                            <div class="wpsh-card-body">
     957                                <?php
     958                                $this->render_toggle_row(
     959                                    'secure_login',
     960                                    __( 'Generic login errors', 'security-hardener' ),
     961                                    __( "Don't reveal whether the username or password was incorrect.", 'security-hardener' )
     962                                );
     963                                $this->render_toggle_row(
     964                                    'rate_limit_login',
     965                                    __( 'Login rate limiting', 'security-hardener' )
     966                                );
     967                                $this->render_number_row(
     968                                    'rate_limit_attempts',
     969                                    __( 'Failed attempts before block', 'security-hardener' ),
     970                                    3, 20, 5,
     971                                    __( 'attempts', 'security-hardener' )
     972                                );
     973                                $this->render_number_row(
     974                                    'rate_limit_minutes',
     975                                    __( 'Block duration', 'security-hardener' ),
     976                                    5, 1440, 15,
     977                                    __( 'minutes', 'security-hardener' )
     978                                );
     979                                ?>
     980                            </div>
     981                        </div>
     982
     983                        <!-- Security Headers -->
     984                        <div class="wpsh-card">
     985                            <div class="wpsh-card-header">
     986                                <h2 class="wpsh-card-title"><?php esc_html_e( 'Security headers', 'security-hardener' ); ?></h2>
     987                            </div>
     988                            <div class="wpsh-card-body">
     989                                <?php
     990                                $this->render_toggle_row(
     991                                    'header_x_frame',
     992                                    __( 'X-Frame-Options', 'security-hardener' ),
     993                                    __( 'Clickjacking protection. Set to SAMEORIGIN.', 'security-hardener' )
     994                                );
     995                                $this->render_toggle_row(
     996                                    'header_x_content',
     997                                    __( 'X-Content-Type-Options', 'security-hardener' ),
     998                                    __( 'MIME sniffing protection. Set to nosniff.', 'security-hardener' )
     999                                );
     1000                                $this->render_toggle_row(
     1001                                    'header_referrer',
     1002                                    __( 'Referrer-Policy', 'security-hardener' ),
     1003                                    __( 'Controls referrer information sent to external sites. Set to strict-origin-when-cross-origin.', 'security-hardener' )
     1004                                );
     1005                                $this->render_toggle_row(
     1006                                    'header_permissions',
     1007                                    __( 'Permissions-Policy', 'security-hardener' ),
     1008                                    __( 'Restricts access to geolocation, microphone and camera.', 'security-hardener' )
     1009                                );
     1010                                ?>
     1011                            </div>
     1012                        </div>
     1013
     1014                        <!-- HSTS -->
     1015                        <div class="wpsh-card">
     1016                            <div class="wpsh-card-header">
     1017                                <h2 class="wpsh-card-title">
     1018                                    <?php esc_html_e( 'HSTS', 'security-hardener' ); ?>
     1019                                    <?php echo $https_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped ?>
     1020                                </h2>
     1021                            </div>
     1022                            <div class="wpsh-card-body">
     1023                                <?php if ( ! is_ssl() ) : ?>
     1024                                    <p class="wpsh-hsts-warn"><?php esc_html_e( '⚠ Your site is not using HTTPS. Do not enable HSTS.', 'security-hardener' ); ?></p>
     1025                                <?php endif; ?>
     1026                                <?php
     1027                                $this->render_toggle_row(
     1028                                    'enable_hsts',
     1029                                    __( 'Enable HSTS', 'security-hardener' ),
     1030                                    __( 'Forces HTTPS. Only enable if your entire site supports HTTPS.', 'security-hardener' )
     1031                                );
     1032                                $this->render_toggle_row(
     1033                                    'hsts_subdomains',
     1034                                    __( 'Include subdomains', 'security-hardener' ),
     1035                                    __( 'Applies the HSTS policy to all subdomains. Only enable if all your subdomains also use HTTPS.', 'security-hardener' )
     1036                                );
     1037                                $this->render_toggle_row(
     1038                                    'hsts_preload',
     1039                                    __( 'Enable preload', 'security-hardener' ),
     1040                                    sprintf(
     1041                                        /* translators: %s: URL to HSTS preload list */
     1042                                        wp_kses_post( __( 'Submit to the <a href="%s" target="_blank">HSTS Preload List</a>. Requires 1 year max-age.', 'security-hardener' ) ),
     1043                                        'https://hstspreload.org/'
     1044                                    )
     1045                                );
     1046                                ?>
     1047                            </div>
     1048                        </div>
     1049
     1050                        <!-- Other Settings -->
     1051                        <div class="wpsh-card">
     1052                            <div class="wpsh-card-header">
     1053                                <h2 class="wpsh-card-title"><?php esc_html_e( 'Other settings', 'security-hardener' ); ?></h2>
     1054                            </div>
     1055                            <div class="wpsh-card-body">
     1056                                <?php
     1057                                $this->render_toggle_row(
     1058                                    'hide_wp_version',
     1059                                    __( 'Hide WordPress version', 'security-hardener' ),
     1060                                    __( 'Removes the generator meta tag and WordPress version from asset URLs (?ver=).', 'security-hardener' )
     1061                                );
     1062                                $this->render_toggle_row(
     1063                                    'clean_head',
     1064                                    __( 'Clean wp_head', 'security-hardener' ),
     1065                                    __( 'Removes RSD link, Windows Live Writer manifest, shortlink, and emoji scripts from &lt;head&gt;.', 'security-hardener' )
     1066                                );
     1067                                $this->render_toggle_row(
     1068                                    'log_security_events',
     1069                                    __( 'Log security events', 'security-hardener' ),
     1070                                    __( 'Keeps a log of the last 100 security events.', 'security-hardener' )
     1071                                );
     1072                                $this->render_toggle_row(
     1073                                    'delete_data_on_uninstall',
     1074                                    __( 'Delete all data on uninstall', 'security-hardener' ),
     1075                                    __( 'Permanently deletes all settings and logs on uninstall. Disabled by default.', 'security-hardener' ),
     1076                                    $warn_badge
     1077                                );
     1078                                ?>
     1079                            </div>
     1080                        </div>
     1081
     1082                    </div><!-- .wpsh-grid -->
     1083
     1084                    <div class="wpsh-save-bar">
     1085                        <?php submit_button( null, 'primary', 'submit', false ); ?>
     1086                    </div>
     1087
    9691088                </form>
    9701089
    9711090                <?php $this->render_security_logs(); ?>
    9721091
    973                 <hr>
     1092                <hr style="margin: 24px 0;">
    9741093
    9751094                <h2><?php esc_html_e( 'Additional Hardening Recommendations', 'security-hardener' ); ?></h2>
    976                 <ul style="list-style: disc; padding-left: 20px;">
     1095                <ul class="wpsh-recommendations" style="list-style: disc; padding-left: 20px;">
    9771096                    <li><?php esc_html_e( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ); ?></li>
    9781097                    <li><?php esc_html_e( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ); ?></li>
     
    9851104                    <li><?php esc_html_e( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ); ?></li>
    9861105                    <li><?php esc_html_e( 'Change the default database table prefix from wp_ to a custom value', 'security-hardener' ); ?></li>
     1106                    <li><?php esc_html_e( 'Rename the default admin account to a non-obvious username', 'security-hardener' ); ?></li>
     1107                    <li><?php esc_html_e( 'Restrict database user privileges to SELECT, INSERT, UPDATE and DELETE only', 'security-hardener' ); ?></li>
     1108                    <li><?php esc_html_e( 'Protect wp-config.php by moving it one directory above the WordPress root or restricting access via .htaccess', 'security-hardener' ); ?></li>
     1109                    <li><?php esc_html_e( 'Block direct access to files in wp-includes/ by adding the following rules to your .htaccess file (outside the WordPress tags).', 'security-hardener' ); ?></li>
    9871110                </ul>
    988 
    9891111                <p>
    9901112                    <?php
     
    9961118                    ?>
    9971119                </p>
    998             </div>
     1120
     1121            </div><!-- .wrap -->
    9991122            <?php
    10001123        }
     
    10141137            }
    10151138
     1139            // Reverse to show newest first, limit to 20
     1140            $logs = array_slice( array_reverse( $logs ), 0, 20 );
    10161141            ?>
    1017             <hr>
    1018             <h2><?php esc_html_e( 'Recent Security Events', 'security-hardener' ); ?></h2>
    1019             <table class="wp-list-table widefat fixed striped">
     1142            <hr style="margin: 24px 0;">
     1143            <div class="wpsh-section-header">
     1144                <h2 style="margin: 0;"><?php esc_html_e( 'Recent Security Events', 'security-hardener' ); ?></h2>
     1145                <a href="<?php echo esc_url( wp_nonce_url( admin_url( 'options-general.php?page=security-hardener&action=clear_logs' ), 'wpsh_clear_logs' ) ); ?>"
     1146                   class="button"
     1147                   onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to clear all security logs?', 'security-hardener' ); ?>');">
     1148                    <?php esc_html_e( 'Clear Logs', 'security-hardener' ); ?>
     1149                </a>
     1150            </div>
     1151            <table class="wp-list-table widefat fixed striped wpsh-logs-table">
    10201152                <thead>
    10211153                    <tr>
    1022                         <th><?php esc_html_e( 'Timestamp', 'security-hardener' ); ?></th>
    1023                         <th><?php esc_html_e( 'Event Type', 'security-hardener' ); ?></th>
     1154                        <th style="width:160px;"><?php esc_html_e( 'Timestamp', 'security-hardener' ); ?></th>
     1155                        <th style="width:140px;"><?php esc_html_e( 'Event Type', 'security-hardener' ); ?></th>
    10241156                        <th><?php esc_html_e( 'Message', 'security-hardener' ); ?></th>
    1025                         <th><?php esc_html_e( 'IP Address', 'security-hardener' ); ?></th>
     1157                        <th style="width:120px;"><?php esc_html_e( 'IP Address', 'security-hardener' ); ?></th>
    10261158                    </tr>
    10271159                </thead>
    10281160                <tbody>
    1029                     <?php
    1030                     // Reverse to show newest first
    1031                     $logs = array_reverse( $logs );
    1032                     // Limit to 20 most recent
    1033                     $logs = array_slice( $logs, 0, 20 );
    1034 
    1035                     foreach ( $logs as $log ) :
    1036                         ?>
     1161                    <?php foreach ( $logs as $log ) : ?>
    10371162                        <tr>
    10381163                            <td><?php echo esc_html( $log['timestamp'] ); ?></td>
     
    10441169                </tbody>
    10451170            </table>
    1046             <p>
    1047                 <a href="<?php echo esc_url( wp_nonce_url( admin_url( 'options-general.php?page=security-hardener&action=clear_logs' ), 'wpsh_clear_logs' ) ); ?>"
    1048                    class="button"
    1049                    onclick="return confirm('<?php esc_attr_e( 'Are you sure you want to clear all security logs?', 'security-hardener' ); ?>');">
    1050                     <?php esc_html_e( 'Clear Logs', 'security-hardener' ); ?>
    1051                 </a>
    1052             </p>
    10531171            <?php
    10541172        }
Note: See TracChangeset for help on using the changeset viewer.