Login Armor

Changelog

2.1.16

Bug fix release addressing four issues raised by an external security review of V2.1.15 (anti-gravity audit, 2026-05-20). Two compatibility fixes for sites running plain permalinks, one audit-trail coverage gap on activity-log events that fire outside an authenticated session (2FA verification, frontend self-registration, password reset via Lost Password), and one coverage gap on third-party login forms.

  • Fix – HideLogin::get_login_url() now branches on permalink_structure, returning https://example.com/?<slug> on plain permalinks instead of the always-rewritten /<slug>/ form. Without this, 2FA redirects and admin-email login links produced a hard 404 on plain-permalinks installs. Mirrors the existing logic in new_login_url() so both helpers stay consistent.
  • Fix – Hardening::require_rest_authentication() (restrict_rest_api toggle) now also inspects the ?rest_route= query parameter — the routing form WordPress uses for REST requests when permalinks are plain. Previously the public-namespace allowlist (oEmbed, Contact Form 7, Site Health) only matched the /wp-json/… rewrite, so every legitimate anonymous REST call was rejected with rest_forbidden (HTTP 403) on plain permalinks. The wp-json/ path detection is preserved for normal permalink installs.
  • Fix – ActivityLogActivityLogger::log() accepts a new optional ?int $explicit_user_id parameter. Specific loggers (TwoFactor, User) now use it to attribute an entry to a known target user even when the request runs in an anonymous context — fixing the silent drop of 2fa_verified, 2fa_failed, 2fa_backup_used, 2fa_device_trusted, 2fa_device_revoked events (fire before wp_set_auth_cookie() during the verification flow) and user_created / user_email_changed / user_password_changed events triggered by frontend self-registration (WooCommerce my-account, native ?action=register, MemberPress, etc.) or by the Lost Password reset flow. The default guard on wp_get_current_user()->exists() still applies when no explicit attribution is provided, so WP-Cron and other system contexts keep being filtered out.
  • Fix – HardeningHoneypot now also injects the honeypot field on WooCommerce native forms (woocommerce_login_form, woocommerce_register_form, woocommerce_lostpassword_form) and emits a small inline script on wp_footer that auto-injects the field into any frontend <form> matching the login/register/lost-password heuristic. Closes the coverage gap on third-party login forms (Elementor Pro Login, Divi Login Module, MemberPress, custom forms posting to wp-login.php). Pure-REST/AJAX login flows that build their request body in JS are not covered by this fallback — those still have to opt in via the existing caught() API.
  • Internal – Documented inline rationale on every modified call site so future maintenance keeps the “why” alongside the “what”.

2.1.15

Bug fix release. Resolves a fatal TypeError triggered when third-party plugins (such as WP Fastest Cache) call WordPress core URL builders (get_site_url, wp_redirect, etc.) with argument types that do not match the strict signatures previously declared by Login Armor’s filter and action callbacks. WordPress core does not validate runtime types on filter callback arguments — so any caller passing a string $blog_id (which is allowed) would crash the admin page on Login Armor’s ?int $blog_id callback. Reported by a user after installing WP Fastest Cache on 2026-05-20.

  • Fix – HideLogin::filter_site_url() no longer enforces ?int $blog_id: all four parameters are now untyped, matching the convention used by WordPress core itself for filter callbacks. The strict string return type is preserved. This is the exact signature that crashed when WP Fastest Cache called get_site_url(‘1’, …) from its “Clear Specific Pages” UI.
  • Fix – Same defensive relaxation applied to six other core WP filter/action callbacks of the same class: HideLogin::filter_network_site_url, HideLogin::filter_wp_redirect (was int $status), HideLogin::filter_login_url (was bool $force_reauth, falsy check switched from false === to empty() for consistent handling of 0, ‘0’, ”, null), LimitLogin::gate_allow_password_reset (was bool $allow, int $user_id), LimitLogin::gate_xmlrpc_enabled (was bool $enabled), ActivityLog UserLogger::on_user_deleted (was ?int $reassign).
  • Internal – Return types remain strictly typed across all touched callbacks: Login Armor still satisfies the contract WordPress expects from filter returns (e.g. site_url returns a string). Only inbound parameters are relaxed. Strictly neutral on canonical WP calls.

2.1.14

Bug fix release. Fixes the prevent_author_enum hardening toggle which was over-broad: it blocked the legitimate ?author=N filter in the WordPress admin Posts/Pages list (“All / Mine / ” links) in addition to the intended public enumeration vector. Reported by jeantelli on the WP.org support forum on 2026-05-19.

  • Fix – Hardening::block_author_query() now early-returns when is_admin() && current_user_can( ‘edit_posts’ ), leaving the core admin author filter on edit.php functional for administrators, editors, authors, and contributors. The frontend enumeration vector (anonymous /?author=N requests, including admin-ajax.php without an authenticated user) remains fully blocked. Aligns with the existing is_user_logged_in() guard pattern used by the four other prevent_author_enum handlers (REST users endpoint, oEmbed user info, user sitemap, REST user prepare).
  • Internal – Strictly neutral on the public enumeration block: identical 301-to-home response for anonymous visitors with ?author=N, anonymous AJAX, and REST author filtering. Three-line change in a single method.

2.1.13

Bug fix release. Fixes a silent 2FA failure on installs whose permalink_structure does not end with a trailing slash (e.g. /%postname%). With 2FA enabled, the verification challenge after submitting login credentials would disappear and the user would land back on the login form with no error message. Reported by a user on 2026-05-11.

  • Fix – PendingCookie::get_path() no longer appends a trailing slash to the Hide Login slug. The cookie was set with path /<slug>/, but the trailing-slash normalisation in HideLogin::handle_loaded() would 302 the verify URL to /<slug>?login-armor-2fa=verify (no trailing slash) on installs where permalink_structure does not end with /. RFC 6265 §5.1.4 path-match then refused to send the cookie on /<slug> because the cookie path /<slug>/ is strictly longer than the request path. maybe_render_verification saw token_data === false and silently bounced to the login URL — exactly the “stuck on login page, no error” symptom. Setting the cookie path to /<slug> (no trailing slash) matches /<slug>, /<slug>/, and /<slug>/… per RFC 6265 while still rejecting /<slug>XYZ.
  • Internal – Strictly neutral on installs with trailing-slash permalinks (the V2.1.0-V2.1.12 majority). No security or functional change for those installs.

2.1.12

Bug fix release. Fixes broken-CSS rendering on the login page when both apex and www hostnames route to the same WordPress without a server-level canonical 301 (common on shared hosting). Two complementary fixes.

  • Fix – HideLogin::intercept_request() now canonicalises the request host before any URI rewriting. If the request lands on a hostname that matches neither home_url() nor site_url() (e.g. example.com/<slug> when siteurl is https://www.example.com), Hide Login issues a 301 to the canonical host preserving the full request URI. Closes the window where WP core’s redirect_canonical (which runs later on template_redirect) was being short-circuited by Hide Login’s plugins_loaded priority 9999 interception. Host-only comparison (not scheme/port) to avoid loop-redirects on misconfigured reverse-proxy setups. Skips WP-CLI, cron and AJAX defensively.
  • Fix – LoginHeaders::build_csp() is now host-aware. Every CSP directive that governs cross-origin resource loads (script-src, style-src, img-src, font-src strict mode, connect-src strict mode, default-src strict mode, form-action) emits ‘self’ PLUS the parsed origins of home_url() AND site_url(). Without this, the previous ‘self’-only policy blocked every wp_enqueue_style()-emitted CSS link whenever the document host differed from the asset host. Defence-in-depth alongside the canonical-host fix. base-uri stays ‘self’ — cross-origin <base> is always a security hole.
  • Feature – New filter login_armor_canonical_host_redirect to skip the V2.1.12 canonical-host 301 when returning false (proxy setups, dev environments, multisite configs with a third routing hostname). Receives $http_host, $home_host, $site_host.
  • Internal – Strictly neutral on installs where home_url() and site_url() resolve to the same single canonical host. Bug reported by Alexandre Puy on dumouriez.com (Dynamixhost / Apache / Debian).

2.1.11

Bug fix release. Fixes a V2.1.9 regression on multisite + domain mapping setups (where sub-sites are mapped to external domains via WP MU Domain Mapping or native WP 4.5+).

  • Fix – HideLogin::build_login_url() is now host-aware: chooses between home_url() and site_url() based on the request’s HTTP_HOST header, instead of always returning site_url() (V2.1.9/V2.1.10 default). Resolves the case where a subsite mapped on an external domain would redirect immediately to /wp-admin/ (404) when typing the slug, because site_url() pointed to the original network path while the request arrived on the mapped domain. Reported on the WP.org support forum by @graphandco. Standard installs (siteurl == home) and multisite headless setups (where the request arrives on the siteurl host) continue to work as in V2.1.9/V2.1.10.
  • Fix – Slug detection in intercept_request() now matches against BOTH home_url($slug, ‘relative’) AND site_url($slug, ‘relative’) (instead of just one). Same dual-base matching extended to the wp-login.php and wp-register.php traps. No security change: relative paths only, no new hostnames accepted.
  • Internal – Filter login_armor_login_url_base (introduced in V2.1.9) is unchanged and continues to wrap the final URL for advanced overrides (third-host scenarios, custom subdomain mapping plugins).

2.1.10

Cosmetic fix release. The 404 page served when an anonymous visitor hits /wp-admin/ with Hide Login enabled now renders as a proper WordPress 404 instead of a half-bootstrapped theme page with a duplicated header.

  • Fix – block_access() now routes through serve_404_template() so the response carries the proper WP_Query::set_404() state. Visible effect: body class error404 is set, Yoast (or any SEO plugin) emits <meta name=”robots” content=”noindex”>, the theme renders its real 404 template instead of a default page layout, and themes with sticky headers (Astra Pro, many FSE themes) no longer show a duplicated header. No security change, no functional change for the actual 404 status header (still 404).
  • Internal – 30 lines of duplicated 404 rendering code removed from block_access(); both code paths now share serve_404_template().

2.1.9

Bug fix release. Hide Login now uses site_url() instead of home_url() to build the rewritten login URL, matching what WordPress core does inside wp_login_url().

  • Fix – Hide Login URL base switched from home_url() to site_url() (14 callsites migrated to a new public static helper HideLogin::build_login_url()). Inherited from the WPS Hide Login fork, the previous home_url() base was invisible on the ~99 percent of installs where siteurl == home, but broke silently on multisite headless (siteurl on admin.example.com, home on example.com), WordPress installed in a subdirectory (/wp/), and reverse-proxy installs with WP_HOME not equal to WP_SITEURL. Reported on the WP.org support forum by @graphandco.
  • Feature – New filter login_armor_login_url_base for exotic setups where neither home_url() nor site_url() matches the hostname that actually serves the login slug (third hostname behind a reverse-proxy, subdomain mapping plugin rewriting the admin URL, etc.). Receives $url, $path, $scheme, $slug.
  • Internal – Strictly neutral on the standard install where siteurl == home. Fresh-install, multisite-headless and WP-in-subdir validation suite passed before tag.

2.1.8

Hygiene release issued from a full V2.1.7 audit. Three findings, all LOW severity, batched in a single update.

  • Fix – admin/views/tabs/settings.php queries the V2.1.1 webhook queue table without an existence guard. On a fresh install where the Activity Log module was never enabled (table not created) or after wp plugin install –force (which doesn’t re-fire activation), every Settings tab load emitted three DB warnings in debug.log (Table ‘X.wp_login_armor_webhook_queue’ doesn’t exist). Non-fatal but log-polluting. Now wraps the SELECT in a SHOW TABLES LIKE guard and returns zero counts when the table is absent.
  • Fix – uninstall.php cleanup list was missing the login_armor_lockout_window option (default 24h, used by LimitLogin::trigger_lockout() for escalation tracking). Plugin deletion previously left this single option behind. Now: zero residue.
  • i18n – Five untranslated strings surfaced by wp i18n make-pot regen: the V2.1.1 Activity Log integrity badge BROKEN, the legacy-rows-not-covered amber notice (singular form), the Breach Check password-found message (singular), the Breach Check email-breach message (singular), and the plugin description meta. All translated to French in languages/login-armor-fr_FR.po, no em dash. .mo recompiled. .pot regenerated against the full source tree (1010 strings vs 990 in 2.1.7) to capture 20 strings that had been added to the code (V2.1.1 webhook + integrity UI) but never made it into the translation template.

2.1.7

Preventive release on the Email 2FA enrollment flow. Closes a self-lockout pattern reported by a user whose hosting silently dropped outgoing mail.

  • Fix – Email 2FA enrollment no longer half-commits when wp_mail() fails. The login_armor_2fa_method = email user meta was previously written before the verification email was attempted, leaving a partially configured state behind on hosts where SMTP is broken (Wanadoo, mutualised hosts without SMTP relay, etc.). The order is now: send first, persist only on success.
  • Feature – New pre-activation modal on the user profile page when a user clicks “Set up Email” 2FA. Forces a real wp_mail() round-trip with a Send-test-email button and a safety-net checkbox (“I have a second admin tab open”) before the Enable button unlocks. Both gates must pass, eliminating the most common cause of admin-locked-out support tickets.
  • Internal – New AJAX endpoint login_armor_2fa_email_test (nonce-protected) sends a one-shot test message without consuming the OTP cooldown.

2.1.6

Preventive release bundling V2.1.5 + post-tag cleanup findings. No bug observed in production — eliminates a latent V2.1.3-style fatal risk in the TwoFactor module and finishes the uninstall.php cleanup audit.

  • Preventive – TwoFactor module now follows the always-require pattern (same as Activity Log V2.1.4 and BreachCheck). Class files are loaded unconditionally so any future hook callback that statically references TwoFactor classes survives fresh installs. Constructor and register() still gated by the enable option — zero-overhead contract preserved.
  • Cleanup – uninstall.php now drops the V2.1.1 webhook queue table, deletes 14 leftover options (HSTS, login headers preset, activity auto-verify daily, V2.1.1 chain init flag + show-notice, 5 webhook), generalizes the transient SQL DELETE to login_armor_* (covering chain_verify_last and any future transient), and clears 3 V2.1.1 cron hooks (webhook dispatch + chain repair + chain auto-verify). Plugin deletion now leaves zero residual data.
  • UX – Activity Log Integrity panel now surfaces “X rows before the integrity chain are not covered by Verify” when legacy or pre-init rows exist. The verify-chain coverage scope is now explicit rather than implicit.

2.1.4

Critical hotfix.

  • Fix – Fatal error Class “LoginArmorActivityLogActivityLog” not found on every fresh install. The class file was loaded only when the Activity Log module was enabled, but the V2.1.1 chain initializer hooked at init priority 5 references the class unconditionally. On a fresh install (Activity Log option not yet set), every request to wp-login.php and the front end crashed with a 500. Existing sites that already had Activity Log enabled were unaffected. Fixed by always loading the class file (constructor still gated — zero-overhead contract preserved) and adding a defensive class_exists guard at the top of maybe_initialize_activity_chain().

2.1.3

Critical hotfix.

  • Fix – Hardening “Hide WordPress version” toggle was stripping the ?ver= cache-buster from LoginArmor’s own admin assets (admin.css, admin.js), in addition to WP core and 3rd-party plugin files. Combined with hosting providers that run a server-side static cache (LiteSpeed LSADC on o2switch PowerBoost, Cloudflare full-page cache, hosting CDNs) keyed on the canonical URL, every LoginArmor update past 2.1.0 was invisible to admins for up to a year of cache TTL — the browser kept fetching the old admin.css from the server-side cache because admin.css (no query) and admin.css?ver=2.1.2 are different cache keys. Filter now whitelists /plugins/login-armor/ paths so our own assets always carry their version-derived hash, while WP core and 3rd-party version disclosure are still stripped.
  • Fix – Defense-in-depth width=”18″ height=”18″ HTML attributes on the Activity Log Integrity bar’s shield SVG icon. Without these, if admin.css fails to reach the browser for any reason (CDN edge stale cache, content-blocker, proxy stripping CSS), the icon defaults to its intrinsic 300x150px and dominates the page layout. The CSS rule is still authoritative; HTML attrs are belt-and-suspenders.

2.1.2

Critical hotfix + UX polish.

  • Fix – Settings tab fatal error on fresh installs that have not yet enabled the Activity Log module. The class LoginArmorActivityLogWebhookDispatcher was referenced in the Settings tab without an explicit require_once, and the file is only loaded when Activity Log is on. Visiting the Settings tab on a default install crashed with Class “LoginArmorActivityLogWebhookDispatcher” not found. Fixed by loading the file unconditionally before its first use.
  • Fix – Save-confirmation toast (Settings saved.) was anchored top-right and overlapped the LoginArmor admin tabs nav, making the message unreadable behind the dark Réglages tab. Moved to bottom-right (Gutenberg snackbar convention), bumped z-index above sticky elements, and re-tuned the entrance animation to slide up from the bottom edge.

2.1.1

  • Feature – Activity Log integrity: every row is HMAC-SHA256 signed and chained to the previous one. Detects any direct-SQL tampering, deletion or insertion. First-in-market for WP audit-log plugins.
  • Feature – Signed webhook forwarding: optional async POST of every activity event to your SIEM, Slack, Datadog, Discord or any HTTPS receiver. X-LoginArmor-Signature HMAC header, adaptive retry policy, max 5 attempts.
  • Feature – WP-CLI command wp login-armor activity verify-chain for scheduled audits and orphan repair (–from, –to, –repair-orphans, –format, –verbose).
  • Feature – Admin UI: new compact “Activity Log Integrity” status bar in Activity tab + full Webhook configuration panel in Settings (URL, secret regenerate, send test event, queue stats).
  • Feature – Login Page Security Headers (CSP + X-Frame + Referrer-Policy) now ON by default on fresh installs; REST public-namespace allowlist filterable via login_armor_rest_public_namespaces; auto-detection of 6 conflicting Hide Login plugins (Rename wp-login.php, WPS Hide Login, Defender, Solid Security, Wordfence, AIOS).
  • Fix – Hardening: mask_login_errors no longer leaks remaining-attempt hint through LimitLogin filter (S-9), Honeypot switched to <input type=”hidden”> to survive theme stripping (S-22), User-Agent truncation cap reduced 500 -> 256 chars (S-19).

2.1.0

  • Security (HIGH) – 2FA pending-verification token no longer travels in the URL. After the password step, the partially-authenticated session is held in a HttpOnly + SameSite=Strict cookie scoped to the login slug, signed with HMAC-SHA256 over wp_salt(‘auth’). Closes a leak surface that exposed the token via browser history, server access logs and the Referer header.
  • Security (defense-in-depth) – The transient that backs the pending session is now keyed on sha256(token) instead of the clear token. The clear token never appears in wp_options.option_name either – DB-read attacks no longer yield a replayable token.
  • Compat – URL token is still accepted as a fallback for one minor (V2.1.0). V2.2.0 will remove the fallback. Browsers that reject SameSite=Strict cookies fall through gracefully.
  • Internal – One-shot upgrade hook purges any leftover V2.0.x 2FA pending transients from wp_options on first load. Idempotent.

2.0.5

Security audit pass. Five fixes identified by an internal Phase 1 + Phase 2 audit against 2.0.4, each double-checked on production before patching. No functional regression.

  • Fix (HIGH) – REST author-enumeration scope. The Hardening “Disable author enumeration” toggle now also gates /wp-json/oembed/1.0/embed, the _embed=1 fanout on /wp/v2/posts, and wp-sitemap-users-N.xml. The lockout-side REST gate also denies oEmbed for locked IPs.
  • Fix (HIGH) – Optional HSTS header for the Login Headers module. Off by default; opt-in 180-day or 1-year-with-subdomains modes. Only emitted on HTTPS requests.
  • Fix (MED) – IPv6 subnet derivation. subnet_of() and DetectionClassifier::compute_subnet() now produce a re-parseable canonical /64 from compressed IPv6 (2001:db8::1 → 2001:db8:0:0::/64). Subnet block rules entered against IPv6 attackers now match.
  • Fix (MED) – Self-DoS via 0.0.0.0 placeholder. record_failed_attempt() and check_lockout() skip the literal placeholder returned by get_client_ip() when the configured proxy header is misconfigured. Prevents site-wide lockout on the shared placeholder.
  • Fix (MED) – “Block PHP in uploads” toggle off no longer wipes user-authored .htaccess rules. Only the LoginArmor block (recognized by # BEGIN/END LoginArmor markers, with a legacy fallback) is stripped.

2.0.4

Real fix for the lockout 429 page never appearing on hosts with a public page cache fronting the Hide Login slug.

  • Fix – LockoutPage::render_on_trigger now performs a 302 redirect to /[hide-slug]/?_la_locked=<timestamp>. The unique query string defeats every public page cache (LiteSpeed Cache, WP Rocket, Cloudflare full-page, hosting reverse-proxy). Verified live with Playwright Firefox 150 + httpx HTTP/2.
  • Fix – Branded lockout page logo now appears (path was referencing a file that did not ship).

2.0.3

Same-day hotfix on top of 2.0.2 for HTTP/2 stream termination.

  • Fix – LoginArmor::flush_response_and_exit() now mirrors WordPress core’s _default_wp_die_handler exactly (no fastcgi_finish_request). Under LiteSpeed/LSAPI, calling fastcgi_finish_request() after a 4xx with a custom body left HTTP/2 streams without END_STREAM, hanging Firefox/Chromium. HTTP/2 lockout response now arrives complete in 1.3 s.

2.0.2

Critical fix for the lockout 429 page.

  • Fix – The branded 429 lockout page now actually reaches the browser when the lockout is triggered. Root cause: PHP’s default output buffer (output_buffering=4096 on most managed hosts) was capturing the body. LoginArmor::flush_response_and_exit() now discards every parent buffer before writing the response. Same helper is used by Hide Login’s 404 renderer and Hardening’s XML-RPC + access-denied responses, so all three paths inherit the fix.

2.0.1

Post-launch patch.

  • Fix – Branded 429 lockout landing page is now rendered on the lockout-triggering attempt (#N), not the next one. New action login_armor_lockout_triggered fires after the lockout marker is committed; LockoutPage subscribes and renders inline.
  • Feature – Two new “Reset” buttons in Settings: “Reset all events & incidents” wipes the events feed, brute-force counters, and incidents in one click; “Reset all activity entries” wipes the admin-action audit trail. Both are confirmation-gated.
  • Assets – WordPress.org banner and icon replaced with the navy-blue / yellow-padlock variant matching the in-admin icon.

2.0.0

First WordPress.org public release of the V2 line. Bundles eight independent security modules in a single sub-megabyte plugin: Hide Login (custom URL slug + branded lockout page), Brute Force Protection (cascading lockouts, subnet blocking, X-Forwarded-For), Hardening (13 one-click toggles), Two-Factor Authentication (TOTP + Email OTP + backup codes + trusted devices + recovery flow), Detection and Incidents (6 attack patterns), Activity Log (compliance-ready audit trail), Login Page Security Headers (CSP / X-Frame-Options / Permissions-Policy presets), and Breach Check (HIBP k-anonymity + opt-in XposedOrNot).

Pre-release security audit:

  • Security – Breach Check email lookup is opt-in by default (only the password lookup is enabled when the module is activated).
  • Security – Reserved-username blacklist folds Unicode-to-ASCII before comparison, so homoglyphs collapse onto the same blacklist entry.
  • Security – REST API gating for unauthenticated users now matches public namespaces by route prefix on the parsed REST path; substring-match query-string bypass closed.
  • Security – “Hide WordPress version” also strips ?ver=X.Y.Z from script and stylesheet URLs at script_loader_src priority 9999.
  • Security – .htaccess writes (Block PHP in uploads, Disable directory listing) are atomic via temp file + rename, with admin notice on flush failure.

Plugin Website
Visit website

Author
wpformation
Version:
2.1.16
Last Updated
May 20, 2026
Active Installs
30
Requires
WordPress 6.8
Tested Up To
WordPress 7.0
Requires PHP
8.1

Share Post

Join our newsletter.

Get insights into what’s happening at ChangelogWP right in your inbox. We don’t believe in spam.