=== WT Hardening === Contributors: webmastersteam Donate link: https://webmasters.team/ Tags: security, hardening, brute-force, xmlrpc, security-headers Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 8.0 Stable tag: 1.0.6 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html WordPress hardening without touching your theme: XML-RPC, user enumeration, login limiter, security headers, strong passwords, event log. == Description == **WT Hardening** is a lightweight, modular security plugin that turns on the most important WordPress hardening measures without modifying your theme, `wp-config.php`, or `.htaccess`. Everything works through WordPress hooks and can be disabled at any moment with a single click. The plugin is **completely free**, without a PRO version, without ads, and without sending data to external servers. Built by the [webmasters.team](https://webmasters.team) crew for daily WordPress work. = Modules = * **XML-RPC** - blocks `xmlrpc.php` (403) and filters the XML-RPC methods. Stops brute-force attacks through the most attacked WordPress endpoint. * **User enumeration** - hides logins by blocking `/wp/v2/users` in the REST API for unauthenticated users. * **Author archive** - redirects `/?author=1` to the homepage (another enumeration vector). * **Hide oEmbed author** - strips `author_name` and `author_url` from oEmbed responses (WPScan-detectable user-enumeration vector) and drops the oEmbed discovery `` tags from ``. * **Hide WP version** - removes the `` tag, `?ver=` query strings from assets, and redundant meta tags (wlwmanifest, rsd, shortlink). * **X-Pingback** - removes the `X-Pingback` HTTP header from all responses. * **Login errors** - replaces the specific "wrong login / wrong password" messages with a single generic text (does not reveal whether a login exists). * **Login limiter** - blocks an IP for X minutes after N failed attempts. Data lives in a dedicated table (not `wp_options`), with hourly cleanup of old entries. * **File editor** - sets `DISALLOW_FILE_EDIT`, hiding the theme and plugin editor in the admin (an attacker who compromises an account cannot inject a backdoor through the browser). * **Password policy** - enforces minimum length and complexity (upper and lower case, digits, optionally symbols) for profile updates, registration, and password reset. * **Security HTTP headers** - `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, optionally `HSTS` (use with care - browsers cache it for a year). * **Event log** - records logins (successful and failed), registrations, user changes, plugin activations and deactivations, theme switches, and settings updates. Retention defaults to 30 days. Visible in the admin panel. = What makes it different = * **Modular architecture** - each module is a separate class and can be disabled in the settings without affecting the others. * **Zero external dependencies** - no Composer, no vendor lock-in, no cloud APIs. * **No data leaves the instance** - everything stays in the local MySQL/MariaDB database. * **Internationalization ready** - full coverage with `__()`/`esc_html__()` and a POT file for translators. * **Clean uninstall** - when the plugin is removed, options, tables, and cron jobs are dropped. = What WT Hardening does NOT do (by design) = * Does not scan for malware - that is a different problem, a different toolset. * Does not ship telemetry to the cloud - all data is local. * Does not add a WAF at the application layer - that belongs to the server or CDN. For those needs we recommend dedicated plugins (e.g. Wordfence) or an infrastructure layer (Cloudflare, fail2ban). == Installation == = Standard install = 1. In the WP admin go to **Plugins → Add new**. 2. Search for "WT Hardening". 3. Click **Install** and then **Activate**. 4. Open the **WT Hardening** menu in the admin (shield icon) and configure the modules. = Manual install (via ZIP) = 1. Download the archive from [wordpress.org/plugins/wt-hardening](https://wordpress.org/plugins/wt-hardening/). 2. In the WP admin go to **Plugins → Add new → Upload plugin**. 3. Pick the downloaded ZIP, click **Install now**, and then **Activate plugin**. = FTP/SFTP install = 1. Unzip the archive. 2. Upload the `wt-hardening` folder to `wp-content/plugins/` on your server. 3. In the WP admin go to **Plugins → Installed plugins** and activate **WT Hardening**. After activation all modules are enabled with sensible defaults. If something clashes with your workflow (for example the Jetpack mobile app that needs XML-RPC), just turn off the specific module in the settings. == Frequently Asked Questions == = Does the plugin modify wp-config.php or .htaccess? = No. Everything is wired through WordPress hooks. Deactivating the plugin instantly reverts all changes (except HSTS, which browsers cache - enabling HSTS is a conscious decision). = Will blocking XML-RPC break my mobile app / Jetpack / pingbacks? = Possibly, if you actually rely on them. Disable the **XML-RPC** module in the settings - the other protection layers stay active. = What happens to the event log on deactivation? = The table stays in the database (useful for later analysis). On full uninstall the tables are dropped cleanly. = Does the plugin work with multisite? = Tested on a single-site install. Multisite should work since we do not use superadmin-specific APIs, but it is not yet formally supported - file an issue if something breaks. = What about performance? = Modules only register the hooks they actually need. The event log is a single INSERT per event. No scanner, no background work beyond an hourly and daily cleanup of old rows. Real impact on response time: under 1 ms. = Why is HSTS disabled by default? = Because `Strict-Transport-Security` has a **sticky** effect - browsers remember the header for a year. Enabling it too early (before your entire traffic works on HTTPS without errors) can lock users out of the site for a long time. Turn it on once HTTPS is rock-solid. = Does the strong password policy apply to users created through WP-CLI or `wp_create_user()`? = No - the policy hooks into admin form validation (profile, registration, reset). Programmatic user creation bypasses these hooks by design. For a typical client workflow (users are created through the admin panel) this is enough. == Screenshots == 1. Settings panel - modules rendered as cards with toggles and descriptions. 2. Login attempt log - currently blocked IPs plus a list of recent failed attempts. 3. Event log - chronology of logins, registrations, user changes, and plugin activations. == Changelog == = 1.0.6 = * **Security: hardened client-IP detection in the login limiter and event log.** Both modules previously trusted the `X-Forwarded-For`, `CF-Connecting-IP` and `X-Real-IP` request headers unconditionally. On any site that is not behind a trusted reverse proxy those headers are attacker-controlled, which allowed the brute-force protection to be bypassed (rotate the header and every attempt is counted as a fresh IP), allowed an attacker to lock out a chosen account, and allowed the IP stored in the security event log to be forged. The client IP is now read from `REMOTE_ADDR` by default; proxy headers are honoured only when you opt in via the new `wt_hardening_trust_proxy_headers` filter (return `true` when WordPress runs behind Cloudflare, nginx or a load balancer that sets those headers). * Fix: minimum password length is now measured in characters (`mb_strlen`) rather than bytes, so multibyte passwords are validated correctly against the policy. * Fix: the event-log cleanup cron event is now also unscheduled on deactivation (previously only the login-limiter cron was cleared). = 1.0.5 = * New module: **Hide oEmbed author** - removes `author_name` and `author_url` from oEmbed JSON responses (a low-effort user-enumeration vector automatically picked up by WPScan) and drops the oEmbed discovery `` tags from `` to reduce the public surface. Enabled by default. = 1.0.4 = * Polish: refined the wording of the "Hide WordPress version" module card on the Settings screen for clarity. = 1.0.3 = * Compatibility: tested and confirmed working with WordPress 7.0 (new "Modern" admin theme, iframed editor, PHP 7.4+ baseline). "Tested up to" bumped to 7.0. No code or behavior changes. = 1.0.2 = * Fix: Plugin URI now points to a public landing page on webmasters.team (the previous URL returned 404 during review). = 1.0.1 = * Fix: readme rewritten in English to satisfy the WordPress.org July 2025 policy. * Fix: escape output in the event log admin page (`EventsPage`). * Fix: replace `parse_url()` with `wp_parse_url()` in the XML-RPC module. * Fix: prefix global variables in `uninstall.php` and add safe-query annotations. = 1.0.0 = First release. * XML-RPC module (endpoint block plus methods filter). * User enumeration blocker for the REST API. * Author archive redirect. * Hide WordPress version. * Remove X-Pingback header. * Generic login error message. * Login attempt limiter with IP lockout. * File editor disable. * Strong password policy (length, mixed case, digits, symbols). * Security HTTP headers (X-Frame, nosniff, Referrer-Policy, Permissions-Policy, HSTS). * Event log (logins, users, plugins, theme, settings) with configurable retention. == Upgrade Notice == = 1.0.6 = Security fix: the login limiter and event log no longer trust spoofable proxy headers (X-Forwarded-For etc.) by default, closing a brute-force bypass and a targeted-lockout vector. Behind Cloudflare/nginx? Enable the new `wt_hardening_trust_proxy_headers` filter. Recommended for all users. = 1.0.5 = Adds a new module that hides the post author's name and URL from oEmbed responses - closes a user-enumeration vector picked up by WPScan. Enabled by default. Only affects sites that expose oEmbed publicly; if you rely on other WordPress sites embedding your posts and want author attribution, disable the new module in the settings. = 1.0.4 = Minor polish to a module description in the Settings panel. No behaviour or data changes. = 1.0.3 = Confirmed compatible with WordPress 7.0. No code or behavior changes. = 1.0.2 = Plugin URI fix - points to the new landing page on webmasters.team. No code or behavior changes. = 1.0.1 = Compliance update: readme translated to English, escape fixes, safer URL parsing. No data or behavior changes. = 1.0.0 = First release - install, activate, modules are enabled with sensible defaults.