WP Manifestindependent plugin directory
manifest / security / mbr-wp-login-customiser

MBR Login Customiser WP Registry grade D+. High-severity findings · 3 findingsWP RegistryHigh-severity findings · 3 findingsOpen the reportD+

Secure and customise your WordPress login page with custom URLs, stunning visual styles, and complete branding control.

★ 3stars
5release downloads
v2.1.0latest · 3mo ago

Install

Download mbr-login-customiser-2.1.0.zip From the v2.1.0 release on GitHub, Jul 14, 2026

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/harbourbob/mbr-wp-login-customiser/releases/download/v2.1.0/mbr-login-customiser-2.1.0.zip
Updates itself. It ships its own updater, so new versions appear under Dashboard → Updates.

🔐 MBR Login Customiser

Lock down and smarten up the part of your WordPress site attackers hit first — the login page.

Login Firewall · Passkeys · Two-Factor · Security Alerts · Trusted Devices · Custom Login URL · Brute-Force Protection



Why it exists

Nine out of ten attacks on a WordPress site start at the same door: /wp-login.php. Bots find it, hammer it, and never get tired. MBR Login Customiser moves that door, bolts it, watches it, puts a firewall in front of it, and — when you're ready — replaces the key with a passkey.

It's a full login-security suite that also happens to make your login page look great. And it does the whole job on your own server:

  • 🆓 Free forever. No premium tier, no locked features, no "upgrade to unlock."
  • 🕵️ No telemetry. Nothing about your site or your users is phoned home. Ever.
  • 🌐 No external services, no CDN. Firewall checks, passkey verification, 2FA QR codes, the lot — all generated and checked in pure PHP, on your box.
  • 📖 GPL and open. Read every line. Fork it. Ship it.

Built and maintained by one person who thinks good security shouldn't be a paywall.


✨ What's new in 2.1

🧱 Login Firewall

Everything else in the suite decides whether to let a login succeed. The firewall acts earlier — on the request itself — so most nasties never reach the form at all. Five layers, each individually switchable:

  1. Request gate — blacklisted, locked-out and firewall-blocked IPs get an HTTP 403 before the login form is even served, instead of only being refused at sign-in.
  2. Bad bot filter — requests with an empty User-Agent, or one matching a known script or scanner (curl, python-requests, sqlmap, WPScan and friends), are turned away. Real browsers always send a User-Agent.
  3. Rate limiting — IPs that hammer the login page are temporarily blocked. This counts page requests, not failed passwords, so it stops floods that never even submit the form.
  4. Honeypot — an invisible field is added to the login form. Humans never see it; any submission that fills it is refused and the IP blocked on the spot.
  5. Minimum fill time — the form carries an HMAC-signed render timestamp. Submissions faster than a human could type — or that never loaded the form at all — are refused. Scoped strictly to wp-login.php, so front-end and custom login forms are completely unaffected.

Plus an optional switch to disable XML-RPC authentication, closing the classic brute-force side door while leaving pingbacks working (off by default — Jetpack and the WordPress mobile apps sign in through XML-RPC).

Firewall events get their own filter in the Logs tab. Whitelisted IPs bypass every firewall check, and there's a wp-config kill switch if you ever need one. As always: pure PHP, no external services.

Previously, in 2.0… (click to expand)

🔑 Passkeys (WebAuthn / FIDO2)

Passwordless, phishing-resistant sign-in using a fingerprint, face, screen lock, or a hardware security key. The genuinely hard part — decoding the authenticator's response and verifying its signature — is done in pure PHP using the platform's own OpenSSL and libsodium. No Composer libraries, no external validation services, and the private key never leaves the user's device. Supports ES256, RS256 and EdDSA credentials, in either passwordless or second-factor mode.

🚨 Security Alerts

Your login log stops being something you have to remember to check. Get an email or a Slack / Discord webhook the moment an IP is locked out, an administrator signs in from an IP not seen before, or failed logins spike past a threshold you set. Per-alert cooldowns mean an ongoing attack can't flood your inbox, and there's a one-click "send test alert" button.

📱 Trusted Devices

Let people tick "trust this device" to skip the second factor on browsers they choose — the friction-killer that stops users disabling 2FA altogether. Backed by an HMAC-signed, server-side revocable cookie with a hard expiry, so trust is time-limited and can be pulled from a user's profile at any time. Only the second step is skipped — the password or passkey is always required.


🧰 The full toolkit

🚪 Access & obfuscation

  • Custom login URL — replace /wp-admin & /wp-login.php with your own slug
  • Block standard login — proper 404s for the default URLs
  • Emergency access — signed backup URL + a wp-config kill switch
  • IP allow / block lists — with full CIDR support (IPv4 & IPv6)
  • Time-based access — only allow logins during set hours, overnight windows included

🛡️ Attack protection

  • Login Firewall — request gate, bad-bot filter, rate limiting, honeypot & minimum fill time, all before a password is checked
  • Brute-force protection — rate limiting with escalating lockouts
  • Offence memory — persistent attackers get progressively longer bans
  • XML-RPC hardening — optionally close the authentication side door
  • Login logging — filterable log with automatic retention
  • Multisite ready — network-wide security policy from Network Admin

🔐 Strong authentication

  • Passkeys (WebAuthn / FIDO2) — passwordless & phishing-resistant
  • Two-factor (TOTP) — works with Google Authenticator, Authy, 1Password
  • Pure-PHP QR enrolment — the enrolment QR is drawn on your server
  • AES-256-GCM encryption of 2FA secrets at rest
  • Recovery codes + admin & wp-config recovery paths
  • Post-login/logout redirects — by role or individual user

🎨 Make it yours

  • Dark mode & glassmorphism form styles out of the box
  • Custom logo, link, welcome message & footer
  • Fonts, animations, shadows and button colours
  • Full custom CSS for anything else
  • Looks like your brand, not a stock WordPress form

🚀 Quick start

  1. Download the latest release (see Releases, or grab it from littlewebshack.com).
  2. In your dashboard go to Plugins → Add New → Upload Plugin, choose the .zip, and Activate. (Or drop the mbr-login-customiser folder into /wp-content/plugins/.)
  3. Open the MBR Login Customiser settings and work through the tabs — each feature is off until you switch it on.
  4. Set your custom login URL first and test it in a private window before logging out. (There's an emergency URL and a kill switch if you ever lock yourself out — see below.)

💡 Tip: turn features on one at a time. Set your custom URL, confirm you can still get in, then layer on the firewall, 2FA, passkeys, alerts and trusted devices as you go. Add your own IP to the whitelist before enabling the firewall and it can never get in your way.


⚙️ A quick tour of the tabs

Tab What it does
Login URL Your custom slug, standard-login blocking, emergency access key
Appearance Logo, colours, dark mode, glassmorphism, fonts, custom CSS
Security Rate limiting, escalating lockouts, IP allow/block lists (CIDR)
Firewall Request gate, bad-bot filter, page rate limiting, honeypot, minimum fill time, XML-RPC
Logs Login event history with retention control — now including firewall events
Redirects Where users land after login / logout, by role or user
Schedule Permitted login hours in your site's timezone
Two-Factor TOTP enrolment, recovery codes, app-password policy
Passkeys Enable WebAuthn, choose passwordless or second-factor mode
Alerts Email / webhook notifications and thresholds
Trusted Devices Enable "remember this device" and set the trust duration

Passkeys and trusted devices are managed per user on their own Users → Profile screen.


🆘 Locked out? Don't panic.

There's always a way back in.

Emergency URL — the backup access link (find it on the Login URL tab and keep it safe):

https://yoursite.com/?mbr_emergency=YOUR_EMERGENCY_KEY

Kill switches — if you have FTP/SSH, add one of these to wp-config.php, log in normally, then remove it:

define('MBR_EMERGENCY_DISABLE', true);        // disables login-URL protection
define('MBR_LOGIN_PASSKEYS_DISABLE', true);   // disables passkeys site-wide
define('MBR_LOGIN_FIREWALL_DISABLE', true);   // disables the login firewall

Recovery codes and administrator overrides are also available for two-factor and passkeys, so a lost phone never means a locked account. And IPs on your whitelist bypass every firewall check, so put your own IP there first.


🔒 Security philosophy

  • Everything happens on your server. No request about your site or your users is ever sent to a third party. Firewall checks, passkey ceremonies, TOTP verification and QR generation are all local, in pure PHP.
  • Additive by default. Passwordless passkey sign-in sits alongside the normal login — it can never lock a legitimate user out.
  • Least surprise. Every feature ships off, and every risky action has a documented recovery path.
  • Independently audited surface. A security-audit report covering the codebase is bundled with the plugin.

What it does not claim to be

  • The Login Firewall protects the login surface at the application level. It isn't a site-wide edge firewall or a CDN — pair it with your host's WAF or Cloudflare for network-level filtering across the whole site.
  • It doesn't replace good hygiene — strong passwords and keeping WordPress updated still matter.

📦 Requirements

WordPress 6.0 or higher
PHP 8.0 or higher (8.3 recommended)
Extensions OpenSSL & libsodium (standard on modern PHP) — needed for passkeys
HTTPS Required for passkeys (a browser rule), recommended everywhere
Multisite ✅ Supported

🔄 Updates

Updates are self-hosted and delivered straight to your dashboard via the Plugin Update Checker — no marketplace account, no telemetry. When a new version is published you'll see the usual update prompt under Plugins, just like any other.


🗺️ Roadmap

Much of the original roadmap is now shipped ✅ — multisite, logging, IP lists, time-based access, 2FA, redirects, brute-force protection and, as of 2.1, the Login Firewall. On the horizon:

  • [ ] Firewall monitor mode — log what would be blocked without blocking it, for cautious rollouts
  • [ ] REST API auth hardening — extending the firewall's coverage to REST authentication endpoints
  • [ ] More authenticator/attestation options for passkeys
  • [ ] Additional alert channels

Got a feature request? Open an issue — this plugin is shaped by the people using it.


📜 Version history (click to expand)

2.1.0

  • New: Login Firewall — five layers of request-level protection that run before a single password is checked: a request gate (403 for blocked and locked-out IPs before the form is served), a bad-bot User-Agent filter, login-page rate limiting, an invisible honeypot, and an HMAC-signed minimum form fill time. All pure PHP with no external services.
  • New: Optional XML-RPC authentication blocking (off by default; Jetpack and the mobile apps use XML-RPC).
  • New: Firewall events in the Logs tab with their own filter, whitelist bypass throughout, and a MBR_LOGIN_FIREWALL_DISABLE kill switch.

2.0.0

  • New: Passkeys (WebAuthn / FIDO2) — passwordless, phishing-resistant sign-in, verified in pure PHP (CBOR decode, COSE key reconstruction, signature checks) with no external services. Passwordless and second-factor modes; ES256, RS256 and EdDSA support.
  • New: Security Alerts — email and Slack/Discord webhook notifications on lockouts, new-IP admin sign-in and failed-login spikes, with per-type cooldowns and a test button.
  • New: Trusted Devices — skip the second factor on trusted browsers via an HMAC-signed, revocable, time-limited cookie.

1.9.x

  • Two-factor authentication (TOTP) with pure-PHP QR enrolment, AES-256-GCM secret encryption, recovery codes and a formal security audit.
  • Multisite support with network-wide policy.

1.x

  • Custom login URL, emergency access, appearance customisation, colour pickers and custom CSS.
  • Login logging, IP allow/block with CIDR, time-based access, redirect rules and brute-force protection.

Full, detailed changelog lives in readme.txt.

🎨 Custom CSS examples (click to expand)

Modern flat design

#login { padding: 5% 0 0; }
.login form {
    border: none;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);
    border-radius: 10px;
}
.login input[type="text"],
.login input[type="password"] {
    border-radius: 5px;
    border: 1px solid #ddd;
    padding: 10px 15px;
}

Dark theme (or just flip on the built-in Dark Mode style)

body.login { background: #1a1a1a; }
.login form { background: #2a2a2a; border: 1px solid #3a3a3a; }
.login label { color: #fff; }
.login input[type="text"],
.login input[type="password"] {
    background: #3a3a3a; border: 1px solid #4a4a4a; color: #fff;
}
.login #backtoblog a, .login #nav a { color: #fff !important; }
🧩 Playing nicely with caching & monitoring (click to expand)

If a caching plugin interferes with your custom login URL, exclude it from caching:

  • WP Rocket → Never Cache URL(s)
  • W3 Total Cache → Never cache the following pages
  • WP Super Cache → Rejected URLs

On SiteGround, also exclude it from SG Optimizer's dynamic cache if needed.

Uptime monitors: if your monitoring service checks the login page with a script (curl or similar), either add its IP to the whitelist or switch off the firewall's bad-bot filter — otherwise it will be refused, exactly as designed.


🤝 Contributing

Issues, ideas and pull requests are all welcome. If you've found a security concern, please report it responsibly rather than opening a public issue.

📄 License

Released under the GPL v2 or later. Use it, study it, change it, share it.

👋 Author

Built with care by Robert Palmer — a solo freelance WordPress developer in Cleethorpes, UK, giving away a suite of genuinely-free, no-nonsense plugins.


If MBR Login Customiser keeps the bots off your door, a ⭐ on the repo is always appreciated.

Releases

6 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v2.1.0 latest
Jul 14, 2026 3mo ago
mbr-login-customiser-2.1.0.zip
2
Jul 4, 2026 3mo ago
mbr-login-customiser-2.0.0.zip
1
Jun 26, 2026 4mo ago
mbr-login-customiser-1.9.1.zip
0
Jun 2, 2026 4mo ago
mbr-login-customiser-1.1.1.zip
1
Jun 1, 2026 4mo ago
mbr-login-customiser-1.1.0.zip
0
May 23, 2026 5mo ago
mbr-login-customiser-1.0.2.zip
1
D+ grade

Security

MBR Login Customiser 2.1.0 · audited by WP Registry

High-severity findings.

1 high 2 medium
Audited release
2.1.0
Findings
3
Worst severity
high
Content hash
a56b39d1b2250a4d755ef49d…

Findings

  • high Passwordless passkey login issues an auth cookie without the authenticate chain

    auth_bypass

    When passkeys are enabled (default mode passwordless), wp_ajax_nopriv_mbr_passkey_login_verify verifies the WebAuthn assertion then calls wp_set_auth_cookie($user->ID) and do_action('wp_login') directly. It never runs wp_signon / apply_filters('authenticate'), and admin-ajax.php never fires login_init. Every other login gate in this plugin is an authenticate or login_init callback: IP whitelist/blacklist/exclusive mode (priority 95), time-of-day schedule (96), lockout that refuses even correct passwords (99), TOTP 2FA (100), and the five-layer firewall (login_init + authenticate 90). Those therefore do not apply to passkey sign-in. Default passkeys_mode is passwordless, so a user who also has TOTP enrolled is signed in without a second factor. Observable: an attacker who possesses a registered authenticator (stolen device, or any enrolled user operating off-network) receives a session cookie from a blacklisted IP, during a lockout, outside allowed hours, and with 2FA enabled. Feature is off until an admin enables passkeys; once on, the nopriv endpoint is unauthenticated.

    includes/class-mbr-login-passkeys.php:61-66,451-559

    Recommendation

    Before issuing the cookie, run the same access/lockout/firewall/2FA decisions used on password POSTs (shared helper, or wp_authenticate_user / authenticate with a dummy password path that still hits those filters). At minimum refuse the sign-in when MBR_Login_Access::is_blacklisted / is_exclusive_mode / schedule-deny / lockout / firewall-block would have refused a password login. If passkeys are meant to stay a second door, document that they bypass those controls and default the mode to '2fa' when TOTP is on. Rate-limit the ceremony by IP using the existing lockout/firewall counters.

  • medium Unauthenticated passkey challenge endpoint writes unbounded transients

    dos

    wp_ajax_nopriv_mbr_passkey_login_options, when passkeys are enabled, generates a random challengeId and set_transient('mbr_pk_lc_' . $chal_id, $challenge, 120) with no nonce, no capability, and no IP/rate cap. The login firewall's rate limiter only counts hits on login_init (wp-login.php / the custom-slug include), so flooding admin-ajax.php?action=mbr_passkey_login_options is unrestricted. Each request inserts a pair of wp_options rows (_transient_mbr_pk_lc_* and timeout) that live two minutes. Observable: an unauthenticated client can grow wp_options (or the object cache) as fast as the web server accepts connections for as long as the feature is on, degrading the site. The companion verify endpoint deletes one row per attempt but cannot keep up with a one-way flood of options.

    includes/class-mbr-login-passkeys.php:431-448

    Recommendation

    Reuse MBR_Login_Firewall::count_hit / MBR_Login_Security lockout against get_client_ip() before inserting a transient; cap outstanding challenges per IP (e.g. one live challenge, overwrite in place); and/or store the challenge in a single hashed option keyed by IP instead of a unique row per request.

  • medium Firewall honeypot and min-fill-time never run on the custom login URL

    insecure_config

    The advertised login path is the custom slug (and emergency URL). MBR_Login_Url::redirect_to_login() require_once's wp-login.php from the init hook of a front-controller request, so $pagenow stays index.php (PHP_SELF is index.php, not wp-login.php). MBR_Login_Firewall::check_submission (authenticate priority 90) returns immediately unless $GLOBALS['pagenow'] === 'wp-login.php', explicitly to spare WooCommerce/front-end wp_signon() forms that lack the hidden fields. That same condition also skips the plugin's own form: honeypot and signed timestamp are still printed on login_form, but a POST to /{slug} never evaluates them. Direct wp-login.php is 404'd for anonymous users, so the only time pagenow is wp-login.php is the token-cookie fallback, not the primary path. Request-level layers on login_init still run (the include does fire login_init). Observable: with the firewall enabled, bots that POST credentials to the custom slug are not refused for filling mbr_url_website or for omitting/forging mbr_fw_ts; those two layers are dead code on the URL the plugin tells admins to use.

    includes/class-mbr-login-firewall.php:301-311

    Recommendation

    Set $GLOBALS['pagenow'] = 'wp-login.php' (and $GLOBALS['wp_did_header'] as needed) immediately before including wp-login.php, or drop the pagenow test and instead set a plugin-owned flag in redirect_to_login() / render_hidden_fields() that check_submission looks for. Keep a separate skip for XMLRPC_REQUEST and for authenticate calls that did not come from this plugin's login form.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.