Changeset 3487446
- Timestamp:
- 03/20/2026 07:58:57 PM (8 days ago)
- Location:
- security-hardener
- Files:
-
- 4 added
- 3 edited
-
assets/blueprints/blueprint.json (modified) (1 diff)
-
tags/2.0.0 (added)
-
tags/2.0.0/readme.txt (added)
-
tags/2.0.0/security-hardener.php (added)
-
tags/2.0.0/uninstall.php (added)
-
trunk/readme.txt (modified) (7 diffs)
-
trunk/security-hardener.php (modified) (20 diffs)
Legend:
- Unmodified
- Added
- Removed
-
security-hardener/assets/blueprints/blueprint.json
r3475624 r3487446 16 16 "pluginZipFile": { 17 17 "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" 19 19 }, 20 20 "options": { -
security-hardener/trunk/readme.txt
r3475624 r3487446 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.2 7 Stable tag: 1.07 Stable tag: 2.0.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 13 13 == Description == 14 14 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. 16 16 17 17 = Key Features = … … 43 43 * `Referrer-Policy: strict-origin-when-cross-origin` 44 44 * `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 46 46 47 47 **Additional Hardening:** 48 * Hide WordPress version 48 * Hide WordPress version (meta generator tag and asset query strings) (meta generator tag and asset query strings) 49 49 * Clean up `wp_head` output 50 50 * Remove unnecessary meta tags and links … … 53 53 > ⚠️ **Important:** Always test security settings in a staging environment first. Some features may affect third-party integrations or plugins. 54 54 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. 56 56 57 57 == Installation == … … 75 75 * Login rate limiting (5 attempts per 15 minutes) 76 76 * Security headers 77 * WordPress version hiding 77 * WordPress version hiding (meta generator tag and asset query strings) 78 78 * Clean wp_head output 79 79 * Security event logging … … 105 105 106 106 When 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) 108 108 109 109 = Does the plugin work with page caching? = … … 160 160 161 161 == 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. 162 172 163 173 = 1.0 - 2026-03-05 = -
security-hardener/trunk/security-hardener.php
r3475624 r3487446 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: 1.06 Version: 2.0.0 7 7 Requires at least: 6.9 8 8 Tested up to: 6.9 … … 20 20 21 21 // Plugin constants 22 define( 'WPSH_VERSION', ' 1.0' );22 define( 'WPSH_VERSION', '2.0.0' ); 23 23 define( 'WPSH_FILE', __FILE__ ); 24 24 define( 'WPSH_DIR', plugin_dir_path( __FILE__ ) ); … … 102 102 add_filter( 'the_generator', '__return_empty_string' ); 103 103 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' ) ); 104 106 } 105 107 … … 201 203 202 204 // Security headers 203 'enable_headers' => 1,204 205 'header_x_frame' => 1, 205 206 'header_x_content' => 1, … … 209 210 // HTTPS 210 211 'enable_hsts' => 0, // Off by default - requires HTTPS 211 'hsts_max_age' => 31536000, 212 'hsts_subdomains' => 1, 212 'hsts_subdomains' => 0, 213 213 'hsts_preload' => 0, 214 214 … … 263 263 */ 264 264 public function send_security_headers(): void { 265 if ( ! $this->get_option( 'enable_headers', true ) ) {266 return;267 }268 269 265 // Prevent sent headers warning 270 266 if ( headers_sent() ) { … … 294 290 // HSTS (only if HTTPS and enabled) 295 291 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 ) ) { 300 295 $hsts_header .= '; includeSubDomains'; 301 296 } … … 319 314 unset( $methods['pingback.extensions.getPingbacks'] ); 320 315 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; 321 333 } 322 334 … … 611 623 remove_action( 'wp_head', 'wlwmanifest_link' ); 612 624 613 // Remove WordPress version614 remove_action( 'wp_head', 'wp_generator' );615 616 625 // Remove shortlink 617 626 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 );621 627 622 628 // Remove emoji scripts … … 677 683 /** 678 684 * 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. 679 689 */ 680 690 public function register_settings(): void { … … 686 696 'sanitize_callback' => array( $this, 'sanitize_options' ), 687 697 ) 688 );689 690 // File Editing section691 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 section704 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 section717 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 section730 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 section771 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 section787 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 section804 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 <head> 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 helper818 *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 field840 *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.OutputNotEscaped858 ! empty( $args['description'] ) ? wp_kses_post( $args['description'] ) : esc_html__( 'Enable', 'security-hardener' )859 );860 }861 862 /**863 * Render number field864 *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 )880 698 ); 881 699 } … … 904 722 'secure_login', 905 723 'rate_limit_login', 906 'enable_headers',907 724 'header_x_frame', 908 725 'header_x_content', … … 933 750 : 15; 934 751 935 $sanitized['hsts_max_age'] = isset( $input['hsts_max_age'] )936 ? max( 300, min( 63072000, absint( $input['hsts_max_age'] ) ) )937 : 31536000;938 939 752 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 940 860 } 941 861 … … 948 868 } 949 869 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>'; 950 874 ?> 951 875 <div class="wrap"> … … 954 878 <?php settings_errors(); ?> 955 879 956 <div class="notice notice-info ">880 <div class="notice notice-info inline" style="margin-top:12px;"> 957 881 <p> 958 882 <strong><?php esc_html_e( 'Important:', 'security-hardener' ); ?></strong> … … 962 886 963 887 <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 & 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 <head>.', '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 969 1088 </form> 970 1089 971 1090 <?php $this->render_security_logs(); ?> 972 1091 973 <hr >1092 <hr style="margin: 24px 0;"> 974 1093 975 1094 <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;"> 977 1096 <li><?php esc_html_e( 'Use strong passwords and enable two-factor authentication', 'security-hardener' ); ?></li> 978 1097 <li><?php esc_html_e( 'Keep WordPress, themes, and plugins updated', 'security-hardener' ); ?></li> … … 985 1104 <li><?php esc_html_e( 'Protect the wp-admin directory with an additional HTTP authentication layer (BasicAuth)', 'security-hardener' ); ?></li> 986 1105 <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> 987 1110 </ul> 988 989 1111 <p> 990 1112 <?php … … 996 1118 ?> 997 1119 </p> 998 </div> 1120 1121 </div><!-- .wrap --> 999 1122 <?php 1000 1123 } … … 1014 1137 } 1015 1138 1139 // Reverse to show newest first, limit to 20 1140 $logs = array_slice( array_reverse( $logs ), 0, 20 ); 1016 1141 ?> 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"> 1020 1152 <thead> 1021 1153 <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> 1024 1156 <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> 1026 1158 </tr> 1027 1159 </thead> 1028 1160 <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 ) : ?> 1037 1162 <tr> 1038 1163 <td><?php echo esc_html( $log['timestamp'] ); ?></td> … … 1044 1169 </tbody> 1045 1170 </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>1053 1171 <?php 1054 1172 }
Note: See TracChangeset
for help on using the changeset viewer.