Plugin documentation

User Security Guard for WordPress

Stops brute-force sign-ins, bots and fake customer sign-ups with automatic IP, network, range and account blocking, a risk engine, a plain-words summary of what the recorded attempts show, a security log, an allowlist, a weekly digest and an optional email sign-in code.

Version 1.7.0 WordPress plugin Paid plugin Requires WordPress 6.2 Requires PHP 7.4 Tested up to 7.1 Updated 26 Oct 2026

1. Overview

User Security Guard for WordPress protects who can sign in to your site and, since 1.7.0, who can sign up. It counts failed sign-ins per IP address, per username and per email address, notices when many addresses attack one account, recognises bots, default usernames and exploit scanners, and scores every sign-in attempt with one risk engine before it lets the request through or blocks it. When an attack changes address on every request, it blocks the whole network the addresses come from and, when the attack hops between neighbouring networks, the wider range around them. Every block stores a reason you can read, and by default a block lasts until an administrator removes it (the one exception is a wide range that the plugin blocks by itself: it lifts after 48 hours). On the WooCommerce My Account and wp-login.php registration forms it refuses account-creation bots, including the ones that solve a CAPTCHA, and Mark as fake learns from a fake account before it deletes it (see Sign-up protection).

This page documents version 1.7.0 (the version in the plugin folder this page was written from). The plugin was earlier called "WP User Security Guard"; a few screen titles, the dashboard widget and the suggested privacy-policy text still say "WP User Security". The admin menu is called User Security and sits right below Users.

It is written for site owners, agencies and WooCommerce shops that want login protection that does one job: it is not a firewall, a malware scanner or a hardening checklist. The defaults work without changes. A small number of switches (the email sign-in code, the country rule, automatic release of blocks, trusting the address administrators sign in from, early refusal, blocking the widest range (an IPv4 /16) and the sign-in notice) are off until you turn them on.

How it works

  1. On wp-login.php, wp-admin, the WooCommerce sign-in forms and XML-RPC the plugin sits on WordPress's authenticate filter (a REST request that carries an application password is checked the same way through a separate core filter; see REST API and application passwords). Before WordPress checks the password it asks one question: is this IP address or this account already blocked? If so the request is refused, even when the password is correct.
  2. After a wrong password the plugin records the attempt and then runs the risk engine, so the attempt that reaches a limit is the one that is judged. The engine adds up points from signals such as failures from the same address, failures against the same account, the same account attacked from many addresses, automation tools and default usernames. It answers allow, suspicious or block.
  3. A block is written to the block list with a plain-English reason. The visitor still sees the same generic message as for any wrong password, so the login never reveals which usernames exist or that a block exists.
  4. An attacker who changes address on every request never reaches a per-address limit, so the plugin also counts different addresses per network (an IPv4 /24, an IPv6 /64) and per wider range (an IPv4 /20 and, if you ask for it, /16; an IPv6 /48 and /32). When enough different addresses of one network or range are refused, or try names that are not accounts, the whole network or range is blocked. Safeguards keep real visitors out of it: see Rotating addresses and ranges.
  5. You see the result on the User Security → Dashboard, in User Security → Security Logs, in throttled email alerts and in an optional daily, weekly or monthly digest email. You lift a block from User Security → Blocked IPs or User Security → Blocked Users, or with wp wpus unblock. The dashboard and the log open with a card, What the recorded attempts show, that says in plain words whether an attack is going on and why nothing has been blocked yet (see What the recorded attempts show).
  6. A sign-up on the My Account or wp-login.php registration form is checked before WordPress creates the account: blocks, whether the page's JavaScript ran, how fast the form came back, a hidden field, the browser, the number of sign-ups from the address and the email domain. A refused sign-up sees the form again with one short message. Every accepted account keeps a short sign-up record, and a brand-new account that probes XML-RPC is blocked. See Sign-up protection.

On an ordinary page view the plugin does not query its own tables. It reads its settings and checks its scheduled jobs (WordPress options). The only front-end request it looks at beyond sign-ins is a request WordPress has already answered with a 404 whose path looks like an exploit probe (see Exploit probing). On the two registration forms it adds a hidden field and a small script; the checks, and the email-domain lookup, run only when the form is sent.

At a glance

Limits per address and per account

Failed sign-ins are counted per IP address, username and email inside a rolling window (60 minutes by default). An address that reaches its limit is blocked on that attempt. Administrator accounts get stricter limits. An account attacked from 10 different addresses is blocked.

One risk engine, readable reasons

Every wrong password is scored. A block needs a configured limit to ask for it, or the score to reach 80. Every block, log row and alert shows why.

Rotating addresses and ranges

A bot that changes address on every request is caught by the network it comes from: 10 different addresses of one IPv4 /24 (IPv6 /64) block the network, 25 addresses spread over three of its networks block the /20 (/48) around them, and the /16 (/32) is an opt-in. A range your customers sign in from, or that holds a trusted address, is left alone.

Blocks stay until you remove them

Nothing lifts a block by itself unless you opt in to automatic release after a number of hours. Blocks you make by hand and permanent blocks never lift on their own. The exception is a wide range that the plugin blocks by itself: it lifts after 48 hours (you can change that).

Bots, scanners and XML-RPC

Automation signals (capped so a normal browser is never blocked on bot grounds alone), default usernames such as admin that do not exist on your site, repeated 404s for /.env and similar paths, and XML-RPC, which is refused with a generic 403 by default.

Sprays: one try per address

Since 1.4.0 every sign-in form carries a signed, time-limited token; a sign-in post without one never loaded the form and is refused before the password is looked at. When many different addresses attack at once, the site is treated as under attack for a while and the limits are halved. The REST users list and ?author=N are closed to visitors, and a real search-engine crawler is told from a fake one by DNS.

Fake sign-ups

Since 1.7.0 the My Account and wp-login.php registration forms refuse account-creation bots that solve the CAPTCHA: no JavaScript, a form sent in a second, a hidden field filled in, a throw-away email domain, a browser years out of date, too many sign-ups from one address. Mark as fake blocks what a fake account came from and deletes it.

Optional email sign-in code

Off by default. Asks for a one-time code, emailed to the account, when someone signs in from a browser the plugin has not seen before. Covers every sign-in form and the roles you tick.

Records and alerts

Security log with 23 event types, a dashboard with a 14-day chart and a plain-words summary of what the recorded attempts show, allowlist, throttled email alerts with a switch for every email on one Emails screen, a security digest, CSV and JSON export, settings import, sign-in diagnostics, and a WordPress privacy exporter and eraser.

What it does not do

  • It is not a firewall, malware scanner, file integrity monitor or hardening score. It looks at authentication, at XML-RPC and REST credentials, and at 404 requests for known exploit paths.
  • It does not add a CAPTCHA (the sign-up checks use a hidden field and a small script that people never notice), hide or move wp-login.php, enforce a password policy, or support authenticator apps. The only second factor is a code sent by email.
  • It does not end sessions that already exist. A block stops new sign-ins; cookies already issued keep working until they expire.
  • It does not block the REST API as a whole. Only requests that carry credentials are looked at.
  • It does not use a country to decide anything. The optional country rule reads a header that your CDN or host adds. Since 1.6.0 the Dashboard also shows where attacks came from, looked up in a small file that ships with the plugin; that is a display only and nothing is sent anywhere.
  • It does not score ordinary page views, browsing the shop, viewing My Account or checking out.
  • It does not look up who owns an address. Per-address limits count the exact address. A network or range is only the block of neighbouring addresses around it (an IPv4 /24, /20 or /16, an IPv6 /64, /48 or /32); the plugin does not know which provider it belongs to (the Dashboard can show a country for display, see Who is knocking).
  • A block, including a network or range block, refuses sign-ins and sign-ups only. Visitors from a blocked network can still browse your site and check out as guests.
  • It does not check accounts created at checkout, by an administrator, over the REST API, by WP-CLI, an import or a social-login plugin. Sign-up protection looks at the My Account and wp-login.php registration forms only.
  • It never blocks an administrator account automatically unless you switch that on, and the automatic rules never create a block for an allowlisted address.

2. Requirements

ItemRequirementNotes
WordPress6.2 or laterFrom the plugin header (Requires at least). Tested up to 7.1 according to the header. The behaviour of core hooks described on this page was checked against WordPress 7.1.2.
PHP7.4 or laterFrom the plugin header (Requires PHP). The Tools screen marks the PHP version with a "Check" flag below 7.4.
DatabaseMySQL or MariaDBThe plugin creates five tables with WordPress's dbDelta() and uses MySQL-style statements (ON DUPLICATE KEY UPDATE, DELETE ... LIMIT).
WooCommerceOptionalDetected automatically (the WooCommerce class or the WC() function). When it is active, the My Account and checkout sign-in forms are protected like wp-login.php, and the My Account registration form gets Sign-up protection. When it is missing, that setting does nothing and Settings says "WooCommerce was not detected on this site, so there is nothing extra to protect. If you install it later, this switches itself on." Checked against WooCommerce 11.1.2.
Email deliveryNeeded for some featuresThe sign-in code, the alerts and the digest are sent with wp_mail(). The plugin works without email, but a sign-in code that cannot be emailed refuses a correct password. Use Send me a test email before turning the code on.
DNS resolverUsed by Sign-up protectionOn a sign-up, the email domain is looked up through your server's own DNS resolver (PHP's dns_get_record()) and the answer is remembered for 12 hours. A lookup that fails or times out never refuses a sign-up.
ThemePrints the page footerThe small script of the sign-up JavaScript check is loaded in the footer (wp_footer()), as WordPress expects of every theme. A theme that never prints the footer would have every sign-up refused with the JavaScript message: untick JavaScript check under Sign-up protection then.
WP-CronNeeded for housekeepingLog and attempt retention, the digest, tidying of expired trusted addresses and remembered browsers run from the daily event wpus_daily_maintenance; the release of expired automatic blocks has an hourly event. Tools warns "WP cron is disabled or overdue, so log retention will not run automatically." when DISABLE_WP_CRON is true or the daily event is not scheduled.
Sign-in logNeeded for network and range blockingThe rules that block a whole network or range ask the log of successful sign-ins whether your customers use it (Settings → Logging → Successful logins, on by default). With that switch off, or Logging off, those rules are paused and say so on Settings and on the dashboard. Per-address and per-account protection do not depend on it.
HTTPSRecommendedSettings and Tools warn "This site is not served over HTTPS. Passwords reach the server unencrypted." when the home URL is not https and the request is not served over HTTPS.
Reverse proxy or CDNYou must tell the pluginBehind Cloudflare or another proxy, turn on User Security → Settings → Reverse proxies → Trust proxy headers and choose the header your proxy sets. See Reverse proxies and the visitor's address.
Other login-limiting pluginsKeep oneThe plugin names these when they are active: Limit Login Attempts Reloaded, Limit Login Attempts, Loginizer, Wordfence Security, Solid Security, All In One WP Security & Firewall, WP Cerber Security, Shield Security, Login LockDown, Login Security Solution. Two of them counting the same failures can lock the wrong person out.
Other two-factor pluginsThis plugin steps asideWhen it finds a class used by Two Factor, Login Security, iThemes Security, Wordfence Login Security, Sucuri or WP 2FA, the email code is not asked at all.
Who can use the screensCapability wpus_manage_securityGranted to the administrator role on activation. See Capability, constant and roles.
MultisiteSupportedTables and settings are per site. See Multisite.

The plugin has no activation check of its own. If the tables are missing, Settings and Tools show "The security tables are not installed. Deactivate and activate the plugin again, or run the schema installer from WP-CLI." The plugin has no WP-CLI command that installs the schema; deactivating and activating the plugin does it.

3. Installation

  1. Download the plugin zip from your account on wpexpertshub.com (My Account → Downloads).
  2. In WordPress, open Plugins → Add Plugin → Upload Plugin, choose the zip file and click Install Now.
  3. Click Activate Plugin. The plugin creates its tables, saves its default settings, grants its capability to administrators and schedules its jobs. It asks nothing during activation.
  4. Open User Security in the admin menu (right below Users). Start with User Security → Allowlist and add your own IP address (the screen shows the address you are browsing from), so the automatic rules never block that address. (It does not help against a block on your account: see Allowlist.)
  5. Open User Security → Settings and read the cards. Behind a CDN or reverse proxy, set Reverse proxies first.
  6. Activate your licence under Plugins → WpExperts Hub Licences to receive updates (next section). The plugin works without it.

Licence and updates

The plugin bundles the WpExperts Hub licence and update client (licence/class-wpxh-licence-client.php, version 2.0.0). It loads only where WordPress is in the admin (this includes admin-ajax.php), in cron runs (update checks) and in WP-CLI. It does not load on a normal page view, a wp-login.php sign-in, an XML-RPC call or a REST request, so a slow licence server can never delay a sign-in the plugin is guarding.

  1. Open Plugins → WpExperts Hub Licences. A Licence link also appears under the plugin's name on the Plugins screen for users who can manage_options. Until a licence is active, the Plugins screen shows an information notice: "Activate your WpExperts Hub licence to receive updates for: User Security Guard for WordPress." with a "Manage licences" link.
  2. Paste the licence key from your purchase email (it is also shown in My Account → Downloads on wpexpertshub.com) and click Activate licence. Only letters, digits and hyphens are kept from what you paste.
  3. New versions then appear on Dashboard → Updates and on the Plugins screen, and install like any other plugin update.
  4. Lost the key? Open Can't find your key? Email it to me on the same screen, enter the email address used for the purchase and click Send licence key.
  5. Before moving the plugin to another live site, click Deactivate licence. The licence is removed from this site even when the licence server cannot be reached.
The plugin works without a licence.No sign-in protection feature checks the licence. The key only unlocks update packages: without an active licence the update notice reads "Automatic update is unavailable for this plugin. Activate your licence to enable updates." What the client sends to wpexpertshub.com, and when, is listed under Privacy.

What activation creates

WhatNameNotes
Database tables{prefix}wpus_attempts, {prefix}wpus_blocks, {prefix}wpus_logs, {prefix}wpus_allowlist, {prefix}wpus_devicesCreated with dbDelta(). Columns are listed under Database tables.
Optionwpus_settingsAdded only when it does not exist yet, with all 79 default values. Existing settings are never overwritten by activation.
Optionwpus_db_versionThe installed schema version (1.2.0 in this release). It is written by the schema check that runs on wp_loaded and admin_init.
Capabilitywpus_manage_securityAdded to the administrator role.
Scheduled eventswpus_daily_maintenance (daily) and wpus_release_blocks (hourly)The first run of each is about an hour after activation. The hourly event is also re-scheduled on wp_loaded if it is missing; the daily event is created at activation, so deactivating and activating the plugin re-creates it.

Activation creates no pages, roles or files. After activation the plugin is working with its default settings.

Updating and deactivating

  • Updating through Dashboard → Updates (with an active licence), or by uploading the new zip under Plugins → Add Plugin → Upload Plugin and choosing Replace current with uploaded, keeps your settings, blocks, logs, allowlist and remembered browsers. A newer schema is applied automatically the next time WordPress loads. Settings keys added by a new version take their default value until you save the Settings screen.
  • Deactivating removes the two scheduled events and nothing else. Tables, settings and the capability stay, but nothing is protected while the plugin is inactive.
  • Coming from 1.0.x: the settings added since then (rotating-address protection, the wide-range rule and the typed-names switch) take their defaults at once, and the defaults are on for the first two, so open User Security → Settings → Login protection after updating and read the cards Block a network that attacks from many addresses and Block a wider range when the attack hops between networks. No data is migrated.
  • Deleting the plugin removes its data only when Delete plugin data on uninstall is ticked (it is ticked by default). See Privacy for the full list.

Multisite

Each site has its own tables (with its own prefix), its own wpus_settings option, allowlist, blocks and log. Network activation runs the activation steps for every existing site; when a new site is created while the plugin is network-active, its tables and scheduled events are created at once, and so is the capability when the person creating the site is a super admin. The capability is granted to a site's administrator role only when a super admin activates the plugin (or creates the site). Every account that can manage_options, and every super admin, counts as an administrator for the alerts and the email code. Deleting the plugin with the uninstall option ticked is evaluated per site, using each site's own setting.

4. Quick start

  1. Install and activate the plugin. Open User Security → Dashboard and read the warnings, if any.
  2. Open User Security → Allowlist, add your own IP address with a label such as "Office" and click Add to allowlist. Only add addresses you control.
  3. Make sure the site has a second administrator account. The Dashboard and Settings warn when there is only one: "This site has a single administrator account. Create a second administrator before tightening blocking rules, so you can always get back in."
  4. Open User Security → Settings → Logging and leave Successful logins ticked (it is by default). The rules that block a network or a range use that log to avoid blocking networks your customers sign in from, and pause themselves without it.
  5. Behind Cloudflare or another proxy: open User Security → Settings → Reverse proxies, tick Trust proxy headers, choose the header your proxy sets and save. Then open User Security → Tools → Sign-in diagnostics and read its message: it says "Working" when the header gives the visitor's address instead of the proxy's.
  6. Open User Security → Emails. Leave Send them to empty to use the site administration address, or enter another address; tick or untick the alerts under Tell me when; keep Send a digest at Every week or choose another schedule.
  7. If another login-limiting plugin is active, keep only one. Settings and Tools name the plugin.
  8. Optional: turn on the email sign-in code. Click Send me a test email first, then tick Ask for a sign-in code, save, and sign in once from a private window to confirm that you receive the code. Remember wp wpus two-factor --disable is the way back in.
  9. If customers can create accounts on your site, Sign-up protection is already on (User Security → Settings → Sign-up protection). A week later open User Security → Security Logs, click Sign-ups, and use Mark as fake on Users for a fake account that still got through.
  10. Activate your licence under Plugins → WpExperts Hub Licences.
  11. A few days later, open User Security → Dashboard. The card What the recorded attempts show counts the addresses that failed or were refused, shows how often the busiest one tried compared with the limit, lists networks that attacked from many addresses and offers Block network or Block range where you want to act yourself.

5. Features

This section describes how each protection behaves, with its defaults. Every setting named here is listed with its key, default and range under Settings. The screens are described under Admin screens.

How a sign-in attempt is judged

Every authentication request goes through the same steps, in this order.

  1. Counting the visit. Each request to wp-login.php, and each submitted WooCommerce sign-in form, is counted in two short-lived counters per address (one for the last minute and one for the last hour). No database row is written for it. On wp-login.php the early refusal in the next step runs first, so a request it refuses is not counted; on the shop form the submission is counted first.
  2. Early refusal (optional, off by default). A blocked address gets a bare 403 before the login page is shown. See Blocks.
  3. authenticate at priority 1: is this already blocked? If the address is on the block list, or the username, the email address or the user ID is, the request is refused before WordPress checks the password. A correct password does not help. A refused country (see Country rule) and XML-RPC authentication that is switched off are refused at this step too. The step is skipped when the username field is empty.
  4. WordPress checks the password. In WordPress 7.1.2 core's own password checks run at priority 20.
  5. authenticate at priority 99: the email sign-in code, when it is switched on and the account is covered. See Email sign-in code.
  6. authenticate at priority 100: the final word. A block decided at step 3 always wins. A correct password is logged as a success. A wrong password is recorded as a failed attempt first and judged by the risk engine second, so a limit counts the attempt that reaches it; then a block or a warning is applied. Whatever happens, the visitor sees the same generic message.

Empty username or password fields are not treated as attempts, and a refused sign-in code is not counted as a wrong password. Another plugin or theme that hooks authenticate later than priority 100 could overrule the plugin's answer. User Security → Tools → Sign-in diagnostics lists such callbacks.

The risk signals

The risk engine runs only after a wrong password. The failed attempt is written down first and judged second, so the counters include the attempt being judged: with a limit of 10, the 10th failed attempt reaches it. (Before version 1.1.0 the engine judged first, so every limit took effect one failure late.)

Signal (code)PointsApplies when (defaults in brackets)
ip_threshold_reached60, and an address blockFailed sign-ins from this address inside the counting window reach the per-IP limit (10), counting the attempt being judged. The address is blocked on that attempt, whatever the total, when the plugin can tell it apart from other visitors (see Reverse proxies); behind a proxy that is not set up it only scores. A request that was turned away because an account or a network was already blocked is not a failed sign-in and does not count. The limit is the same whoever the account is: it is not lowered for administrator accounts. For XML-RPC the limit is the lower of the per-IP limit and the XML-RPC limit.
ip_failures_suspicious25The count reaches the "suspicious" limit (5; 4 when the account is an administrator) but not the per-IP limit.
multiple_usernames_from_ip15Five or more different usernames of real accounts were tried from this address.
multiple_accounts_from_ip20Three or more different accounts (username and email pairs) were tried from this address.
probing_activity15Two or more exploit-path requests from this address inside the exploit-probing window. See Exploit probing.
account_threshold_reached (admin_account_threshold for an administrator)60Failed attempts against this username or email reach the account limit (20; for administrators 5). Asks for an account block.
account_failures_suspicious20Failures against this account reach the "suspicious" limit (5) but not the account limit.
distributed_account_attack70The account was attacked from as many different addresses as the "attacked from this many IPs" limit (10; 3 for administrators). Asks for an account block.
admin_account_targeted20The account is an administrator and has at least one failed sign-in (the attempt being judged counts).
default_username15A default username such as admin that is not an account on this site was tried. See Default usernames.
default_username_repeated60The same address reached the default-username limit (3) and is one identifiable client. Asks for an IP block.
bot_indicatorsup to 35Automation signals combined, capped at 35. See Bot detection.
repeat_offender0The address was blocked automatically, released by the clock inside the repeat-offender memory, and is failing again: its per-IP limit and its "suspicious" limit are halved. See Blocks.
hard_blocked_ip, hard_blocked_account100The address or account is already on the block list.

The decision. A request is blocked when a rule above asks for a block (the per-IP limit, an account limit, a distributed attack, repeated default usernames) or when the total reaches the block score (Risk score for "block", 80). It is marked suspicious when the total reaches Risk score for "suspicious" (25) but not the block score. Otherwise it is allowed. An account limit or a distributed attack asks for an account block (see Administrator protection for the exception for administrators); the per-IP limit and repeated default usernames ask for an address block; a block caused only by the score blocks the address. An allowlisted address is not scored at all. With Login protection switched off nothing is scored.

A suspicious result still lets the request take its course (it was a wrong password anyway). It writes a "Suspicious authentication activity" log entry, at most one per address and username every 10 minutes, and fires the wpus_suspicious_activity action. Only the distributed-attack shape (distributed_account_attack) is worth an email.

Four worked examples with the default settings

  1. Someone guesses the password of a real, ordinary account from one address with a normal browser. Until the 5th failed attempt nothing happens. On the 5th, the address reaches the "suspicious" limit (25 points) and so does the account (20 points): 45 points, so the attempt is marked suspicious. On the 10th failed attempt the address reaches its limit: 60 points plus 20 for the account, and the address is blocked with the reason "10 failed logins from this IP address (threshold 10)".
  2. Someone guesses an administrator account. The administrator limits apply (5). On the 5th failed attempt the account limit (60 points) is reached, the address scores 25 as suspicious (its "suspicious" limit is 4 here; the limit that blocks an address is still 10), and 20 more are added for targeting an administrator. The plugin would block the account, but administrator accounts are not blocked automatically by default, so it blocks the address instead and logs that the account block was refused. You get the alert "Administrator block refused" (see Alerts).
  3. A script tries many made-up usernames from one address. Unknown usernames are not stored by name, so only the per-address limit applies: the address is marked suspicious on the 5th failed attempt and blocked on the 10th. (Before version 1.1.0 such a script was only ever marked suspicious, because the limit added 60 points and the block score is 80.) A script that announces itself as curl gets 20 extra points, and together with missing browser headers the bot signals add up to their cap of 35. A default name such as admin adds 15 points per attempt and blocks the address on the third attempt.
  4. A script changes address on every request. No address ever reaches a limit, so nothing above fires. The rotating-address rule counts the different addresses of one network instead: the 10th different address of an IPv4 /24 (or IPv6 /64) that is refused or tries a name that is not an account blocks the whole network, and a bot that also hops between neighbouring networks is stopped at the /20 around them (see Rotating addresses and ranges).
The per-IP limit blocks by itself.Reaching Block after failed attempts from one IP blocks the address on that attempt (sign-ins only; browsing is not affected). Before version 1.1.0 it only added 60 points and the address was blocked only if something else added at least 20 more, so a script that guessed names that are not accounts was never blocked however often it tried. Bot signals alone still cannot reach the block score unless you set Risk score for "block" to 35 or lower.

Login limits and counters

The counters that feed the limits are rows in the attempts table. A row is written for every failed sign-in and for every request refused because an address, an account or a network was already blocked (a refused country is logged but not written here). Successful sign-ins are written to the security log only.

  • Per address. Failed sign-ins from the same IP address inside the counting window, including the attempt being judged. A request that was turned away because the address, the account or its network was already blocked is written down as well, but it does not count toward this limit (it counts toward the rotating-address rule). Default window: 60 minutes (Counting window (minutes)). Rows older than the window stop counting but stay in the log.
  • Per username and per email. Failed rows only, so an attacker who is already blocked cannot lock the real owner out by being refused. For an ordinary account the account limit is the higher of the username limit and the email limit (20 each by default). For an administrator account it is the username limit, lowered to the administrator limit.
  • Distinct addresses per account. The number of different IP addresses with failed attempts against the same username or email inside the window. When it reaches Block an account attacked from this many IPs (10) the account is blocked as a distributed attack. The failed attempt that brings the number of different addresses to ten triggers it.
  • A manual unblock restarts the counters. After you unblock an address, account, network or range, only attempts made after the unblock count; unblocking a network or range restarts the count of every overlapping network and range as well, so the block cannot be recreated a moment later by the network inside it or around it. The old rows stay for the detail views and the audit trail. The same applies when automatic release lifts a block. Counters also restart when you use Delete all attempt rows on Tools.
  • Retention. Attempt rows are deleted after Keep attempt rows for (30 days). The saved value is never lower than the counting window rounded up to whole days, because shorter retention would make every limit undercount.
  • What is not stored. A username that does not exist is not stored by name. Text typed into the username box that contains an @ is treated as an email address and stored as typed (lower-cased, first 100 characters), whether or not it is a real address.

Switching Login protection off stops scoring, the recording of failed attempts, blocking at sign-in, the early refusal, and the rewriting of error messages. XML-RPC blocking, the exploit-probing rule and the email code are separate and keep working. The country rule, and the refusal of XML-RPC password logins while Allow XML-RPC authentication is off, are part of the sign-in check and are skipped too.

Administrator protection

An account counts as an administrator when it can manage_options or has the administrator role. This is used only inside the plugin: it is never reflected in an error message or a response.

  • Stricter limits (Administrator protection ticked, default on). For an administrator account the username and email limits are lowered to Failed attempts before an admin account is treated as targeted (5) if they are higher. The distinct-address limit becomes half of that, rounded up (3), and the "suspicious" limit becomes that value minus one (4). The per-IP limit that adds 60 points is not lowered: the risk engine always reads Block after failed attempts from one IP (10) for it, even though the Settings screen sentence "Administrator accounts use the stricter limit of 5 attempts instead of 10" suggests otherwise.
  • Never blocked automatically by default. With Automatically block administrator accounts off (default), a rule that asks for an administrator account block is refused. The plugin then blocks the source address instead, provided that address has itself failed at least as many times as the "suspicious" limit, so the owner's first typo cannot block an address because an attacker used up the account's failures. The refusal is logged as suspicious activity, fires wpus_account_block_refused and sends the alert "Administrator block refused".
  • Turning automatic blocking on means an attacker who reaches the limit can lock that administrator out until you unblock the account by hand. Settings warns about this and the Dashboard shows "Admin accounts auto-blocked: Enabled". Keep a second administrator account, and know wp wpus unblock --account=<login>.
  • Dedicated log events. Administrator sign-ins are logged as "Administrator login" and failures as "Administrator login failure" (each can be switched off separately). These events feed the Dashboard card Administrator activity and the "Admin failures" tile.

One generic error

Unknown username, unknown email, wrong password and every refusal by the plugin return one message: "The username or password you entered is incorrect." The filter wpus_generic_login_error replaces the text.

  • Only credential errors are rewritten (invalid_username, invalid_email, incorrect_password and the plugin's own refusal codes). Empty-field messages, password-reset messages, registration errors and other plugins' messages are left alone.
  • REST requests with bad credentials get HTTP 401, the error code incorrect_password and the same message, so the REST API cannot be used to find out which usernames exist.
  • Because blocked and wrong-password answers look the same, a customer who is blocked sees "incorrect password". Check User Security → Blocked IPs and User Security → Blocked Users when someone says their correct password is refused.
  • Password reset is never blocked (it is not a sign-in). It is also never refused by the early refusal, never challenged for a sign-in code and never affected by the country rule.

Bot detection

Bot detection produces signals, not decisions. Each signal adds points when a wrong password is scored, and the combined bot contribution is capped at 35. With the default block score of 80, a normal browser is never blocked on bot grounds alone. Signals are only looked at on the sign-in endpoints (wp-login.php, XML-RPC, REST with credentials and the WooCommerce sign-in form).

SignalPointsWhen
Automation user agent (automation_user_agent)20The user agent contains one of: curl/, wget/, python, python-requests, urllib, httpx, aiohttp, go-http-client, java/, libwww-perl, ruby, php/, node-fetch, axios, okhttp, guzzle, scrapy, nikto, sqlmap, masscan, zgrab, nmap, hydra, medusa, wpscan, dirbuster, headlesschrome, phantomjs, selenium, puppeteer, playwright, splash. Not applied to requests that use Basic authentication (application passwords and API integrations).
Missing browser headers (missing_browser_headers)15A sign-in POST carries at most one of the four headers Accept, Accept-Language, Accept-Encoding and Connection.
Few browser headers (thin_browser_headers)8A sign-in POST carries exactly two of those four.
No user agent (empty_user_agent)10A sign-in request has an empty user agent.
High frequency (high_frequency)12The address made at least Requests per minute that count as high frequency (10) requests to the sign-in endpoints in the last minute.
Extreme frequency (extreme_frequency)25Four times that rate or more in a minute.
Sustained hammering (sustained_frequency)15Six times the per-minute figure or more in the last hour (60 requests by default).
XML-RPC (xmlrpc_automation)10The request is an XML-RPC authentication attempt.
  • When a failed attempt carries bot signals, a "Bot / automation detected" log entry is written with the signal codes.
  • A burst on the login page. When one address reaches the high-frequency rate on wp-login.php (every page view and submission counts) or on the WooCommerce sign-in form (submissions only), the plugin writes a "Bot / automation detected" entry (at most once per address every 5 minutes) and fires wpus_attack_spike, which sends the "A burst of traffic on the login page" alert. A sustained burst of GET requests to wp-login.php is also run through the risk engine, so the configured rules decide whether the address is blocked.
  • The frequency counters are fixed windows kept in transients (a minute and an hour from the first hit), so slow traffic is not mistaken for a burst.
  • Switching Bot detection off removes the signals from scoring. The request counters and the burst alert still run while Login protection is on.

Default usernames

Password-guessing tools start with names such as admin. A real visitor has no reason to try a default name that is not their own, so a failed sign-in with one is stronger evidence than an ordinary wrong password.

  • Which names. admin, administrator, root, test, demo, user, guest, webmaster, wpadmin, sysadmin, superadmin, owner, wordpress and adm; the same name followed by up to six digits, with an optional -, _ or . in between (for example admin1, admin_2024, root-01); and the part before the @ of an email address (admin@example.com). The filter wpus_generic_usernames changes the list.
  • Only names that are not accounts. If the submitted name is a username or email of a real account, the rule never applies. A site whose administrator really is called admin is protected by the ordinary limits, and Settings suggests renaming it: "An account called admin exists on this site. Because it is a real account it is not covered by this rule, and it is the first name every attacker tries."
  • What it does. Each failed attempt adds 15 points. The plugin also counts these attempts per address inside the counting window. Once an address has made Block the address after this many attempts (3), the engine adds 60 points and asks to block the address. The counter is updated before the engine runs, so the third attempt itself triggers the block.
  • Only for one identifiable client. The block applies only when the address is attributable (see Reverse proxies). Behind a proxy that the plugin was not told about, each attempt keeps its small 15-point weight and nothing more, because blocking the proxy's address would lock out everyone behind it.
  • Allowlisted addresses are never counted. A manual unblock starts the count again. The visitor still sees the same generic message.

Exploit probing (404 requests)

Scanners ask for files such as /.env, /.git/config, wp-config.php.bak or phpmyadmin long before they try a password. On a healthy site every one of those requests answers 404. This rule counts them and blocks the address from signing in when it keeps asking.

  • Narrow by design. It runs on template_redirect (priority 0) and only when WordPress has already decided the request is a 404. A page that exists, an ordinary broken link and a query string (the query string is never looked at) are never counted. The visitor still sees your normal 404 page. A path your web server answers with its own 404 never reaches WordPress and is invisible to the plugin.
  • What counts as a probe. The path is URL-decoded twice, lower-cased and matched against these patterns: environment files (.env), version-control folders (.git, .svn, .hg, .bzr), hidden config files (.htaccess, .htpasswd, .ds_store, .npmrc, .aws, .ssh and similar), copies of wp-config (.bak, .old, .zip, ~ and similar), PHP info pages (phpinfo.php), database admin tools (phpmyadmin, adminer, pma), backups and dumps (backup.zip, db.sql), log files (debug.log, error_log), web shells (shell.php, c99.php), cgi-bin, vendor/phpunit and similar test runners, path traversal (../, /etc/passwd), project files (composer.json, package.json, docker-compose.yml, web.config), and server status pages (server-status, actuator). The filter wpus_probe_rules lets you remove or add patterns (label and regular expression).
  • What happens. The first probe from an address inside the window writes a "Suspicious authentication activity" log entry ("Request for a file that exploit scanners look for (<kind>) answered 404"). Two or more probes add 15 points to that address's later sign-in attempts. At Block after this many probes (5) inside Counting window (minutes) (10) the address is blocked with the reason "N requests for exploit paths that answer 404 within M minutes (last: <kind>)". That is an ordinary automatic block: it stops sign-ins from that address and does not stop it browsing your site.
  • Who is ignored. Signed-in users who can edit posts, allowlisted addresses, addresses that are already blocked (no database write for them) and addresses that are not one identifiable client (see Reverse proxies).
  • Counters are kept in transients, not table rows, and a manual unblock clears the count.

XML-RPC

XML-RPC is blocked by default (Block XML-RPC, on). It is a legacy endpoint that amplifies credential attacks and almost no site needs it.

  • While blocked, every request to xmlrpc.php is refused before WordPress parses it, with HTTP 403 and the generic text "Access to this service is not available." under the title "Forbidden". The plugin also switches off WordPress's xmlrpc_enabled filter. This applies to every address, including allowlisted ones.
  • Each refused request is recorded as a refused attempt, except from an allowlisted address, and logged as "XML-RPC blocked" (since 1.4.0 one log line per address per ten minutes; every request is still an attempt row, so every limit counts every one of them). When an address has this many refused attempts inside the counting window, XML-RPC attempt limit per IP (5), it is blocked with the reason "XML-RPC hammering: N blocked XML-RPC requests (threshold 5)". Each refused request fires wpus_xmlrpc_blocked, which sends the alert "XML-RPC is being hammered": one email per Repeat window (minutes) for the whole site, whichever address the request came from, naming the latest address and how many requests were held back since the last email (a scanner that rotates addresses used to cause one email per address). Many different addresses of one network that hit XML-RPC end in a network block (see Rotating addresses and ranges).
  • To allow XML-RPC (for Jetpack, the mobile app or another tool) untick Block XML-RPC. Password logins over XML-RPC then stay refused until you also tick Allow XML-RPC authentication: a refused XML-RPC login is logged as "Blocked login" with the reason "XML-RPC authentication is disabled". With both on, XML-RPC logins go through the normal checks, and the per-IP limit for them is the lower of the general limit and the XML-RPC limit.
  • With XML-RPC allowed, every call is logged by method name (never the arguments, which routinely contain passwords), at most one entry per address and method per minute. Untick Log XML-RPC activity to stop that, or XML-RPC requests under Logging.
  • Settings and the Dashboard warn "XML-RPC is allowed" while it is allowed.

WooCommerce sign-in forms

A wrong password on the My Account page or the checkout login has always been counted, scored and blocked like one on wp-login.php, because WooCommerce signs people in through WordPress. With Protect the shop sign-in form on (default, and only when WooCommerce is active) the plugin also recognises the form as a sign-in page:

  • A submission is recognised by its shape: a POST with the fields login, username and password. WooCommerce's nonce is not required, because a bot that leaves it out is still attacking the form. The request is labelled with the endpoint woocommerce in the attempts table and the log.
  • It is counted in the same request counters, so bursts raise the same "burst of traffic" alert, and missing browser headers and an empty user agent are scored (see Bot detection).
  • With Blocked addresses (early refusal) on, a blocked address is refused with a 403 on this form too.
  • The email sign-in code appears on this form, in WooCommerce's own markup, and the optional sign-in notice is printed at the end of the form.
  • Viewing My Account, browsing the shop and checking out are ordinary page views and are never scored. The plugin observes the form; it does not replace it.

REST API and application passwords

The REST API is never blocked as a whole. Only requests that carry credentials are looked at.

  • In WordPress 7.1.2 a REST request with Basic credentials has its application password checked directly (on determine_current_user), not through the authenticate filters. The plugin hooks application_password_is_api_request to enforce blocks: a blocked address or account cannot authenticate with an application password. The caller is unauthenticated and gets the endpoint's ordinary 401 or 403.
  • A failed application-password attempt is recorded and scored like a failed sign-in, using the Basic-auth username as the identifier. A successful one is logged as a sign-in. Basic-auth requests are not given the automation user-agent signal.
  • Credential errors from REST authentication are replaced by one generic 401 answer (see One generic error).
  • The email sign-in code is never asked for REST, application passwords, XML-RPC, cron or WP-CLI, because they have no form. A covered account's application password therefore keeps working.

Blocks

A block is a row in the block list with a reason. A refused sign-in never changes the visitor's answer: they see the generic message.

What gets blocked

  • IP address, network or range (type ip). A value such as 203.0.113.9 is one address; 45.133.7.0/24 is a network and 178.239.192.0/20 a range, and everybody in it is refused at sign-in. Listed on User Security → Blocked IPs, where a network carries a network mark and a range a range /20 mark. See Rotating addresses and ranges.
  • Account (type account for the username, the email address and the identifier that was typed, and type user for the numeric user ID). One account block writes up to three rows, so the account cannot come back through a different sign-in method. All of them are listed on User Security → Blocked Users, while the tab counts each account once. Unblock clears every row of the account together; Delete removes only the row you chose and leaves the others active.

Statuses

  • Blocked: active. A block that will lift by itself (automatic release is on, or it is a wide range the plugin blocked) shows a countdown ("lifts in 3 hours"; "lapsed — release pending" once the time is up but the hourly job has not tidied it yet).
  • Permanent: active and never lifts by itself.
  • Unblocked: history. The row is kept; blocking the same address or account again re-activates the same row (keeping the first-detected date) and increases its block count.

Reasons

Every block stores a readable reason. Examples: Risk score 80 reached the blocking threshold of 80; Account "bob" reached 20 failed logins (threshold 20); Account "bob" was targeted from 10 different IP addresses (threshold 10); 3 login attempts with default usernames such as "admin" that do not exist here (threshold 3); 5 requests for exploit paths that answer 404 within 10 minutes (last: environment file); XML-RPC hammering: 5 blocked XML-RPC requests (threshold 5); 10 failed logins from this IP address (threshold 10); Rotating addresses: 36 different addresses in 45.133.7.0/24 were refused or tried unknown accounts within 60 minutes (limit 10); Rotating addresses: 25 different addresses in 178.239.192.0/20, spread over 4 of its networks, were refused or tried unknown accounts within 60 minutes (limit 25). A manual block without a reason says "Blocked manually by an administrator" (from wp wpus block: "Blocked from WP-CLI"). The reason column holds 191 characters; reasons you type are cut to fit.

Manual blocks

  • User Security → Blocked IPs → Block an IP address manually: enter an address, a network or a range and a reason, tick Flag as a permanent block if you want one (it starts ticked when Allow permanent blocks (IP blocks) is on). Type any address followed by /24, /20 or /16 (IPv6: /64, /48 or /32) to block the network or range it belongs to; other sizes are answered with "A network must be written as an IPv4 /24, /20 or /16 (203.0.113.0/24) or an IPv6 /64, /48 or /32 (2001:db8:1:2::/64)." The plugin refuses your own current address ("That is your own IP address. Blocking it would lock you out of this site, so it was not blocked."), a network or range that holds your own address ("Your own IP address is inside that network. Blocking it would lock you out of this site, so it was not blocked."), an invalid address ("Enter a valid IPv4 or IPv6 address, or a network such as 203.0.113.0/24."), an allowlisted address ("This IP address is on the allowlist. Remove it from the allowlist before blocking it.") and a network or range with a trusted address inside ("A trusted address on the allowlist is inside this network. Remove it from the allowlist before blocking the network."). From the command line: wp wpus block --ip=178.239.192.0/20.
  • User Security → Blocked Users → Block an account manually: enter a username or email address and a reason. The username, the email address and the user ID are blocked together. If the text matches no user, only the typed value is blocked.
  • Manual blocking in Settings gates both forms and wp wpus block: when it is off they answer "Manual blocking is disabled in the plugin settings." Unblocking still works, and the forms are still shown.
  • A block made by an administrator (screens or WP-CLI) never gets an expiry and can replace an active automatic block, for example to make it permanent. An automatic rule never rewrites an active block; it only updates its counters.

Row actions and bulk actions

Each row offers View activity (the Security Logs filtered to that address or username), Unblock, Make permanent (only on active blocks that are not permanent yet) and Delete. The bulk menu offers Unblock (manual), Flag as permanent and Delete record. A confirmation appears before an unblock, a delete or a bulk action, and choosing a bulk action with nothing ticked shows "Select at least one row first, then choose an action."

  • Make permanent needs Allow permanent blocks for that kind of block (IP blocks or account blocks) to be ticked; otherwise nothing changes and the notice says "Permanent blocks are switched off for this kind of block in Settings, so nothing was changed." A permanent block loses any expiry.
  • Unblock works at once ("The entry can sign in again immediately") and restarts the counters for that address or account. Unblocking a network or range restarts the count of every overlapping network and range.
  • Delete removes the block row and its history permanently. On an active IP block that also lifts the block. For an account it removes only the row you chose; the other rows (username, email, user ID) keep the account blocked, so use Unblock to let it sign in again.

Automatic release (opt-in)

Set Release automatic blocks after to a number of hours (0, the default, means never). Then:

  • A block that a rule created by itself, and that is not permanent, gets an expiry that many hours after it starts. Enforcement stops the moment the time is up: expired blocks are not enforced and drop out of the Active list even before the hourly job (wpus_release_blocks) tidies the row. The job then marks the row Unblocked with the reason "Released automatically", writes an unblock log entry and fires wpus_block_unblocked.
  • Manual and permanent blocks never get an expiry. The counters restart when a block lifts.
  • The expiry is set when the block is created. Blocks that already existed when you switched automatic release on carry no expiry and never lift by themselves, and changing the number of hours later does not change the expiry a block already has.
  • Emergency mode pauses everything: blocks never lapse and new ones get no expiry while it is on. wp wpus release runs the release on demand.
  • A wide range lifts by itself, even with automatic release off. A range the plugin blocked on its own (an IPv4 /20 or /16, an IPv6 /48 or /32) gets an expiry of Release a wide-range block by itself after hours (48 by default; 0 keeps it until you unblock it), because a wrong guess there refuses thousands of people. A network (/24, /64) follows the ordinary automatic release above. A block you make by hand never expires. This is the one place where the plugin lifts a block without being asked to, and it was added on purpose (version 1.2.0).

Longer every time (opt-in)

Longer every time (1 to 10 times; 1 means every block lasts the same) makes each repeat automatic block last that many times longer than the last: with 24 hours and a factor of 3 the lengths are 24, 72, 216, 648 hours, and never more than 8,760 hours (a year). Settings previews the lengths from the values saved. It only matters when automatic release is on. The ladder continues only when the clock ended the earlier block and the address or account was blocked again inside Remember it for days of that block ending. A block an administrator lifted, a block made by hand, or a quiet spell longer than the memory starts again at the base length. A block that is still running is never lengthened.

Repeat offenders

With Repeat offenders on (default) and automatic release in use, an address that was blocked automatically, released by the clock inside the last Remember it for days (30) and is failing again has its per-IP limit and its "suspicious" limit halved (rounded up, never below 1). An address you unblocked yourself is never a repeat offender. Only those two numbers move (the "suspicious" limit is also the one that decides the 20-point account step, so that step moves with it); the account block limits stay.

Early refusal (opt-in)

With Blocked addresses ticked (default off), a blocked address gets a plain 403 ("Access to this page is not available.", titled "Forbidden") on wp-login.php and the WooCommerce sign-in form, and "Access to this service is not available." in plain text on XML-RPC when XML-RPC is allowed, before the page loads. It saves a little work per bot request but announces the block, which is why it is off. Password reset and logout are never refused. Each early refusal is logged at most once per address every 10 minutes.

Blocks do not end sessions.A block stops authentication. Cookies that were already issued keep working until they expire.

Rotating addresses and ranges

A hosting provider hands out whole networks of addresses, and a botnet or proxy service can spread one attack over dozens of addresses that each try once or twice. Every address stays below its own limit, so nothing is blocked, while the network they come from is plainly hostile. This rule counts different addresses per network and blocks the network, and, when the attack hops between neighbouring networks, the wider range around them. It is on by default and affects sign-ins only: visitors from a blocked network can still browse your site and check out as guests.

The ladder

SizeIPv4IPv6Blocked when (defaults)
Network/24 (256 addresses)/6410 different addresses of it were refused or tried names that are not accounts within the counting window (Block a network that attacks from many addresses; 3 to 500).
Wide range (on by default)/20 (4,096 addresses)/4825 addresses (2.5 times the network number), spread over at least 3 of its networks.
Widest range (off by default)/16 (65,536 addresses)/3260 addresses (6 times the network number), spread over at least 3 of its /20 (IPv6: /48) ranges.

The same evidence is counted at every size, from the narrowest outward, and the narrowest range that shows the attack is the one that is blocked. A bot that uses 178.239.198.0, then 178.239.198.15, then 178.239.194.0 shares a /24 only for the first two, and a counter per /24 never sees all three: the /20 around them does.

What counts

  • Requests to xmlrpc.php that were refused, sign-ins with a name that is not an account on your site, and requests that were turned away because the address, its network or the account was already blocked.
  • A failed password for a real account never counts: a customer's typo says nothing about the network they are on.
  • Each address counts once, however often it tries, and only inside the counting window (60 minutes by default).

How a range is counted

A narrower network counts toward the range around it for at most as many addresses as it takes to block that network on its own, less one (9 of the 25 by default). One crowded network plus a couple of stray addresses elsewhere is therefore not an attack on the whole /20, while an attack that really is spread over the range adds up (9 + 9 + 7 = 25). The /16 does the same one level up. The block records how many addresses were seen and how many counted. The numbers follow the setting: a network number of 4 gives 4 for a /24, 10 for a /20 and 24 for a /16.

Emergency mode caps the network number at 5 (a /24 blocks at 5 addresses, a /20 at 13) and switches the wide rule on. The counting caps still come from the number you saved, so emergency mode blocks a range at least as early as normal mode, never later.

Safeguards for real visitors

A rule that blocks many people at once must be hard to trigger by accident. The plugin leaves a network or range alone, and says why in the Security Logs (once an hour per network), when:

  • the address that triggered the check is not one the plugin can tell apart from other visitors (see Reverse proxies), or is a trusted address;
  • a trusted address or an Allowlist range lies inside it;
  • somebody signed in successfully from inside it in the last 90 days, as far back as the sign-in log is kept. A sign-in from an address that is itself among the attacking addresses does not count (a bot that signs in with a stolen password from one of its own addresses does not protect its range);
  • the successful sign-ins of customers are not being logged. Then nothing can tell a network your customers use from one only attackers use, so the rule is paused, and Settings and the dashboard card say so. Tick Settings → Logging → Successful logins again to resume.

You can still block such a network or range yourself from the summary card or from Blocked IPs. You cannot block a range that holds your own address. An address on your allowlist is never refused because of a network or range block.

One sign-in is enough to protect a range.A single successful sign-in from anywhere inside a range leaves it alone for 90 days, whoever it was: a customer, a visitor who registered once, or an attacker with a working account who signs in from a quiet address. If you see an attack from a range the plugin reports as left alone, block it yourself from the summary card.

How a block ends

  • A network (/24, /64) follows the ordinary rules: it stays until an administrator removes it, or lifts after Release automatic blocks after when you set that.
  • A wide range (/20, /16, /48, /32) that the plugin blocked lifts by itself after Release a wide-range block by itself after (48 hours by default), even when automatic release is off. Set 0 to keep it until you unblock it. Blocks created while emergency mode is on get no expiry.
  • A block you make by hand never expires. An unblock, or a block that ran out, restarts the count of every overlapping network and range.

Where you see it

Blocked IPs lists a network with a network mark and a range with a range /20 mark (hover for the number of addresses); the Usernames tried column shows how many addresses were seen. The block email names the network or range and says when it lifts. The Security Logs address field takes a network or range and matches its members exactly. With WP-CLI: wp wpus block --ip=178.239.192.0/20. The action wpus_network_left_alone fires when a network is left alone on purpose (see Actions).

What the recorded attempts show

When the log fills with failed sign-ins and nothing is blocked, the question is whether the plugin is broken or the attack is spread too thin for a per-address limit. The card What the recorded attempts show, at the top of the Dashboard and above the Security Logs, answers it from the attempt rows (as far back as attempts are kept, 30 days by default):

The card What the recorded attempts show: counts, the busiest address against the limit, one plain sentence and the networks that attacked from many addresses with their status and a Block range link
The summary card with demo data: 91 failed or refused sign-ins from 66 addresses; a /20 range, a /24 and an IPv6 /64 network are blocked, a second /20 has 7 addresses in 2 networks (not blocked yet, with its Block range link), and a /24 is blocked by the /20 around it.
  • how many failed or refused sign-ins were recorded, from how many different addresses, how often the busiest address tried compared with the limit that blocks one, and how many blocks exist right now;
  • one plain sentence: either an address that is under its limit, or an attacker who keeps changing address. When the total proves nothing (a total over days cannot be compared with a limit that counts attempts within minutes) the sentence says what is known and what is not;
  • the networks that used three or more attacking addresses, with their status and a Block network link, and, when an attack is spread over a wide range, the range with a Block range link that says how many addresses that is. Both use the ordinary audited block action with its own confirmation.

A network that should not be blocked is marked and explained instead: a trusted network, a network a real person signed in from, your own network, and, with no link at all, a private range and the edge of a proxy or CDN the plugin was not told about (every visitor behind it arrives from the same few addresses, so a block would turn them all away).

Who is knocking: login attempts, top countries, top blocked IPs

Since 1.6.0 three cards sit on the Dashboard between the 14-day chart and the latest events. They only read what the plugin already records; nothing new is stored and nothing is written.

The three Dashboard cards: Login attempts with a Successful and Failed switch, Top countries by number of attacks with bars, and Top IPs blocked with a 24 hours, 7 days and 30 days switch
The three cards with demo data (the addresses are documentation addresses and the countries are made up for the picture): the latest successful sign-ins, the countries the last seven days of failed and refused sign-ins came from, and the networks and addresses blocked in the last 24 hours.
  • Login attempts. A switch between Successful and Failed. Each row shows the username (an Admin tag for an administrator's sign-in), the address with the flag of its country, and how long ago: "3 hours 45 minutes ago" within a day, the date and time after that. The list scrolls inside the card, shows the latest 50 of each and links to the full log. Successful comes from the security log, so it is only filled while Log successful logins is on (Settings, Logging); otherwise the card says so and links to Settings. Failed shows the name that was typed, which may not be an account.
  • Top countries by number of attacks. The failed and refused sign-ins of the last 7 days grouped by country: a bar, the number of attempts, the number of different addresses and the share. Private addresses and addresses with no known country are one Unknown or private network line. The ten biggest countries are listed, with "and N more" when there are others.
  • Top IPs blocked. A switch between 24 hours, 7 days and 30 days lists the addresses and whole networks that were blocked in that time, most attempts first, with their status (Blocked, Permanent or Released); a released block is included because the card answers "who was blocked", not "who is blocked now". The attempt count opens that address on Blocked IPs. With nothing to show it says "No blocks have been recorded."

Where the countries come from. A small file inside the plugin (data/country-ranges.dat, about 5 MB), a conversion of the free IP to Country Lite database by DB-IP.com (Creative Commons Attribution 4.0, credited under the card). Every address is looked up on your own server, so nothing is downloaded or sent anywhere, and old attempts get a country too. It is a display only: nothing is blocked, allowed or scored by it, and the country rule still reads only the header your CDN or host adds. A VPN or a mobile network can show another country than the person's own. Developers can replace the answer with the filter wpus_country_for_ip.

On a phone the rows of Login attempts and Top IPs blocked become two short lines instead of a table that scrolls sideways.

The three cards at phone width, side by side: each row is two short lines
The same cards at a true phone width of 390 pixels.

Sign-in form check (form origin)

The password sprays that reach a site are credential lists posted straight at wp-login.php or the shop's My Account form, one try per address, by clients that never load the form. Since 1.4.0 every sign-in form the plugin knows (wp-login.php, a form made with wp_login_form(), the WooCommerce sign-in form) carries a hidden field wpus_form with a signed, time-limited token. A sign-in post that carries no valid token never loaded the form, so it is refused before the password is looked at, with its own message ("This sign-in form had expired or was not loaded from this site. Please reload the page and try again."), and recorded as a refused attempt that reads "form not loaded", not as a failed sign-in.

  • The token proves "a form of this site was rendered recently" and nothing else. It is not bound to the address (a phone changes network between loading and sending) and not single-use (a wrong password re-renders the form with a fresh one). A token of the WooCommerce form is worthless on wp-login.php and the other way round. It stays valid for A form stays valid for (hours), 24 by default.
  • Only the two interactive forms are checked, and by their shape (log and pwd on wp-login.php; username, password and login for WooCommerce). REST, application passwords, XML-RPC, the lost-password form and a plugin that signs people in on its own page are never asked for a token. A hand-made form that posts to wp-login.php without WordPress's login_form hook can be exempted with the wpus_form_origin_required filter.
  • Refusals are their own ladder: an address the plugin may act on that posts without the form as many times as Block after failed attempts from one IP inside the counting window is blocked ("Posted the sign-in form N times within M minutes without loading it"). Refusals never count toward the failed-password limit, but they do count for the rotating-address rule and the distributed-attack rule. An allowlisted address, or one that is not one client, is refused but never blocked for it.
  • Emergency mode forces the check on. The action wpus_form_origin_refused fires for every refusal.

Sign-up protection (fake customer accounts)

Account-creation bots load the registration form, read its hidden fields, solve the CAPTCHA (an image code is read by OCR) and post the form like a person, from a new address almost every time; minutes later many of them probe xmlrpc.php. They never fail a password, so the sign-in rules on this page never see them. Since 1.7.0 the plugin checks the two interactive registration forms before WordPress creates the account: the WooCommerce My Account form and wp-login.php?action=register. Everything is on by default except the alert email; the switches are under User Security → Settings → Sign-up protection (see the settings).

  • Only those two forms. An account created at checkout, through the Store API, by an administrator, over the REST API (wp/v2/users, wc/v3/customers), by WP-CLI, an import or a social-login plugin is never looked at. An allowlisted address passes every check. The filter wpus_registration_required exempts a form.
  • Nothing changes for a person. The form gets one hidden field that a small script fills in, and one field people never see. A refused person sees the same form again with one short message, never an error page: "We could not create your account. Please reload the page and try again." (or, when the page's JavaScript did not run, "We could not create your account. Please enable JavaScript in your browser, reload the page and try again."). The filter wpus_registration_refused_message changes it.
  • Nothing is shipped as a list. There is no built-in list of bad addresses, domains or mail servers and nothing is downloaded: the plugin learns from the accounts you mark as fake.

The checks, in order

The first check that says no decides. The email domain is looked up last, so a bot stopped earlier costs no lookup.

CheckRefuses a sign-up whenReason in the logSwitch
BlocksThe address, or a network or range around it, is blocked; or the typed email address or username is blocked.address or network blocked; email or username blockedalways (with the master switch)
Hidden fieldA field nobody sees was filled in. It sits off screen, is hidden from screen readers, cannot be reached with the Tab key, has autocomplete off and is labelled "Leave this field empty". A post without the field is not evidence, and the value is never stored.hidden field filled inHidden field
JavaScriptThe form's signed token reaches its hidden field only through the plugin's small script (in the page the field is empty and the token sits, reversed, in an attribute). A tool that posts the fields it read from the page without running its scripts posts it empty. A token older than A form stays valid for (hours) (24, under Sign-in form check) counts as missing too.form posted without JavaScriptJavaScript check
Fill timeThe form came back sooner than 4 seconds after it was shown (the token carries the time).form sent too fastMinimum time to fill in the form
BrowserNo browser name, an automation tool's name, or a Chrome-based browser or Firefox 24 versions (about two years) behind today's.no browser name; automation tool; browser years out of dateOutdated browser
Sign-ups per addressThe address already made 3 accepted sign-ups in the last hour. Counted only for an address that is one identifiable client.too many sign-ups from one addressSign-ups per address
Email domainThe domain is, or is under, a domain on Blocked email domains; it refuses all mail (a "null MX"); it has no mail server and no address at all; it receives mail for any made-up name (a wildcard MX); or its mail arrives on a server on Blocked mail servers.email domain blocked; email domain refuses all mail; email domain cannot receive mail; throw-away email domain (wildcard MX); email domain uses a blocked mail serverEmail domain (two switches) and the two lists
  • The browser estimate. Chrome and Firefox release a new major version every four weeks and update themselves; account-creation tools keep one browser name for years (the bot this feature was built against called itself Chrome 103 on Windows 8.1 for ten months). Today's version is worked out from the browsers' release calendar (Chrome 157 and Firefox 158 on 26 October 2026); nothing is downloaded. Safari, browsers on iOS other than Chrome and Firefox, and browsers the plugin does not recognise are never judged. The filter wpus_browser_current_major corrects the estimate.
  • The email-domain lookups. Only when a sign-up is sent, only for the email domain, through your server's own DNS resolver, remembered for 12 hours per domain (10 minutes after an error). To notice a wildcard MX the plugin also asks once for a random name under the domain's parent (wpus-<random>.example.net): a domain that answers for a name nobody chose takes mail for any name. A lookup that fails or times out never refuses. Providers whose customers really receive mail at any sub-domain (Fastmail and privacy-alias services such as SimpleLogin, addy.io and Firefox Relay) are not treated as throw-away (filter wpus_wildcard_mx_exempt).
  • Addresses that keep trying. An address whose sign-ups are refused as many times as Block after failed attempts from one IP (10) within the counting window is blocked ("Sign-up refused N times within M minutes (limit L)"). Refused sign-ups also count for the rotating-address rules, so a wave from one network blocks that network.
  • What is written. A refusal writes one "Sign-up refused" log line with the reason (switch: Requests refused by a block) and one attempt row (endpoint wc-register or wp-register), and fires wpus_registration_refused. An accepted sign-up writes a "Sign-up" log line (switch: Log sign-ups) and keeps a short sign-up record on the account: the time, the address, the browser, the form, whether the address was one client, and the mail servers of the email domain. The record is part of the privacy export and erasure.
  • New accounts that probe XML-RPC. An account younger than 60 minutes that requests xmlrpc.php, recognised by its sign-in cookie or because it signed up from the address that asks, is blocked: an ordinary account block with the reason "New account probed XML-RPC N minutes after sign-up", whether XML-RPC itself is blocked or allowed. The account is never deleted, and accounts in a watched role and administrators are never blocked by this. A session the account already has is not ended (blocks never end sessions).
  • On a sign-in a browser years out of date is only a bot signal of 15 points, inside the 35-point cap of all bot signals: it never blocks a sign-in on its own.
Security Logs filtered to the kind Sign-ups: accepted sign-ups with a Mark as fake link, refused ones naming the check that refused them (no JavaScript, hidden field, throw-away domain, outdated browser, a domain that cannot receive mail, a blocked domain), and one account marked as fake
Security Logs → Sign-ups with demo data (documentation addresses, .example domains): customers who signed up, a bot wave refused by a different check each time, three bot accounts that got through, one of them marked as fake, and the next sign-up from its domain refused at once.

Mark as fake

Some fake accounts will still get through: a bot with a current browser name and a domain that is not a wildcard looks like a person. Mark as fake turns each one into protection against the next. It is offered under every account on Users → All Users (a row link and a bulk action) and under an accepted sign-up in the Security Logs, and as wp wpus mark-fake <user>. On the screens it always opens a confirmation panel first: each account with when it registered, the address it signed up from and its browser, and what will happen to it.

The Mark as fake confirmation panel on the Users screen: two bot accounts that will be marked, with the address each signed up from, and a customer who has orders and is left alone; one red button Mark 2 accounts as fake and delete them, and Cancel
The confirmation panel with demo data: two accounts will be marked; a customer who has orders is left alone, with the reason.

For each account it then:

  1. blocks the address the account signed up from (an ordinary manual IP block, listed under Blocked IPs);
  2. adds the mail servers the email domain runs itself (a mail server named under the same domain) to Blocked mail servers, so the next made-up domain on the same server is refused too;
  3. adds the domain without its random first part (kehofo.magical.surf becomes magical.surf) to Blocked email domains;
  4. deletes the account, writes "Account marked as fake" to the log and fires wpus_user_marked_fake.
  • It never learns a free mail provider (Gmail, Outlook, Hotmail, Yahoo, iCloud, Proton, GMX and the like; filter wpus_never_learn_domains), your own domains, a mail server shared by many domains, an allowlisted, private, CDN or proxy address, or the address you are working from.
  • It refuses your own account, an account in a watched role (Emails → Accounts to watch; administrators always), an account with orders (it is a customer) and an account that wrote content. The panel says why.
  • An account made before 1.7.0 has no sign-up record: the newest browser session WordPress keeps for it stands in for the address, and the panel says so.
  • Everything learned is listed under Settings → Sign-up protection; delete a line to forget it. The address block is lifted like any other, under Blocked IPs.

Emergency mode

Emergency mode switches every sign-up check on, doubles the minimum fill time (at least 4 seconds), halves the sign-ups per address (no limit becomes 2), keeps the browser limit at 24 versions at most and the XML-RPC watch at 60 minutes at least.

Distributed attacks (sprays)

Every per-address rule sees one address, and the network rules one network. A botnet tries once or twice from each of many addresses in unrelated networks with names that are not accounts, so each rule on its own sees nothing. Since 1.4.0 the plugin also looks at the whole site: after each hostile sign-in event (a failure with a name that is not an account, or a form posted without being loaded) it counts, at most once a minute, the different addresses with such a row on the sign-in forms inside Counting window (60 minutes). At Different addresses that start it (8) the site is treated as under attack for Keep the tighter limits for (60 minutes), extended each time the count is still over the threshold.

  • While it lasts, the failed-sign-in limit and the suspicious limit per address are halved (never below two tries; every risk result carries the signal Distributed attack in progress), and the rotating-address rule needs half as many addresses of one network.
  • Nobody is blocked for the state alone. A customer's wrong password on a real account never counts, and XML-RPC has its own rules.
  • You get one email per episode ("Distributed sign-in attack: N addresses", under Notifications), one log line, and the card What the recorded attempts show on the Dashboard and above the Security Logs opens with a red Under attack line that says until when.
  • The state ends by itself. wp wpus spray shows it and wp wpus spray --off ends it early; the rows that started it cannot start it again.

Account discovery (REST users list, ?author=N)

A spray starts with a list of names. WordPress hands them to a visitor in two ways that no visitor needs: the REST users list and the ?author=N redirect that answers with the author's page. Since 1.4.0 both are closed to visitors who are not signed in (Account discovery under Settings, two switches).

  • REST users list: /wp-json/wp/v2/users and /wp-json/wp/v2/users/<id> are a missing route for a request without a user. /wp/v2/users/me, signed-in users, the block editor and application passwords keep everything. A theme that lists authors through the REST API for visitors would need the switch off.
  • Author numbers: a visitor's ?author=N (numeric, with pretty permalinks) is answered with the site's ordinary 404 page instead of a redirect to the author's address, and counts as an exploit probe ("author number"). oEmbed answers stop naming the author. Author archive pages at their normal addresses are untouched; nothing here ever answers 403. With plain permalinks the switch does nothing, because ?author=N is then the author page's real address.

Search-engine crawlers

Anybody can send a Googlebot User-Agent. Since 1.4.0 a client that claims to be Googlebot, Bingbot, Applebot, DuckDuckBot, YandexBot or Baiduspider is verified the way the engines document it: the address's reverse DNS name must end in the engine's domain and resolve back to the same address. The answer is remembered for a day per address; a browser User-Agent is never looked up, and the lookup only runs where the plugin would otherwise act (a probe, a refused XML-RPC request, a failed sign-in).

  • A verified crawler is never counted by the exploit-probing rule (a real crawler asks for odd URLs it found on other sites), so a search engine can never collect a block on your site.
  • A fake one gets no exemption, and its log rows say "claims to be Googlebot (not verified: it is not one)".
  • With Crawler verification off, every crawler is treated like any other client. Nothing the plugin does touches a page a search engine reads: refusals happen on wp-login.php, the shop's sign-in form and xmlrpc.php, and the plugin never answers 403 for a page, a 404 or a public REST route.

Allowlist

Allowlisted addresses are exempt from the automatic rules. Open User Security → Allowlist, enter an address or range and an optional label, and click Add to allowlist.

  • What you can enter. An IPv4 or IPv6 address (203.0.113.10), a CIDR range with a prefix of 1 to 32 (IPv4) or 1 to 128 (IPv6) (10.0.0.0/24, 2001:db8::/32), or IPv4 shorthand 10.*, 10.0.* or 10.0.0.* (stored as 10.0.0.0/8, 10.0.0.0/16 and 10.0.0.0/24). Wildcards must be at the end. Duplicates are refused. The list shows the address as stored, whether it is an address or a range, the label, who added it and when.
  • What it skips. An allowlisted address is never scored, never flagged as a bot on a failed sign-in, never counted for default usernames or exploit probing, never gets an automatic block (a rule that tries is refused), is not recorded for XML-RPC escalation, and is never refused by the country rule. It is never counted toward a network or range block and is never refused because of one (a network block cannot be lifted for one member; an exact block of the address itself is lifted with Unblock). Manual blocking of an allowlisted address, or of a network or range with an allowlisted address inside, is refused.
  • What it does not skip. It does not skip the email sign-in code. It does not lift a block that already exists: an address that was blocked before you allowlisted it is still refused at sign-in until you unblock it. An account block applies to an allowlisted address too, including one created later by an attack from other addresses. It does not exempt anyone from the XML-RPC 403 while XML-RPC is blocked. It does not switch off the login-page burst check: a burst of requests from an allowlisted address is still logged as "Bot / automation detected" and still sends the burst alert (see Bot detection). It never makes a wrong password right.
  • It is a gap on purpose. Only add addresses you control. A shared office or café address trusts everyone behind it. The Dashboard and Settings warn "Allowlisted IP addresses are exempt from scoring, new IP blocks and the country rule. They are not exempt from account blocks or from a block that already exists. Every allowlist entry is a deliberate gap in the security controls." while the list is not empty.
  • Entries you type never expire. Every add and removal is written to the security log, whether it came from the screen, WP-CLI or the plugin itself. The list is cached for 10 minutes and the cache is cleared on every change.

Trusted administrator addresses (opt-in)

With Trust the address administrators sign in from ticked (default off), the address an administrator has just signed in from is added to the allowlist, so your own typos, or an attacker guessing your password while you are away, cannot block the address you use.

  • Only a completed, interactive sign-in counts (wp-login.php, wp-admin and the WooCommerce form). Never REST, application passwords or XML-RPC, and never a sign-in that the email-code safety net voided.
  • Only an administrator (an account that can manage_options), and only a public address that is one identifiable client (see Reverse proxies). Loopback, private and proxy addresses are never trusted.
  • The entry is labelled "Auto-added after administrator sign-in (<login>)" and expires after Trusted for (days) (30). Each sign-in from the address renews it. An expired entry stops counting at once and is removed by the daily job.
  • Each administrator keeps at most Addresses kept per administrator (3) automatic entries; the oldest is removed when a new one is added.
  • An address that is already allowlisted by hand, or inside a range, is left alone. Deleting or demoting the account removes its automatic entries. Emergency mode pauses the feature.
  • The filter wpus_trust_admin_ip lets you refuse an address (for example a company VPN exit) or accept one the checks refuse.

Country rule (opt-in)

The country rule refuses sign-ins from countries you choose, or from every country you do not choose. The site, the login page and password reset stay reachable and anyone already signed in stays signed in.

  • Rule: Off (default), Block sign-ins from the countries below or Only allow sign-ins from the countries below.
  • Where the country comes from: one header your CDN or host adds. The choices are CF-IPCountry (default), CloudFront-Viewer-Country, X-Vercel-IP-Country, X-AppEngine-Country, GeoIP-Country-Code and GEOIP_COUNTRY_CODE. Nothing else is ever read. The rule never uses the country file of the Dashboard cards; it reads this header only. Only trust a header that nothing in front of your site lets a visitor set: with no CDN, anyone could send it and the rule protects nothing. Settings shows what the current request carried.
  • Countries: a filterable list of 250 ISO 3166-1 two-letter codes (the United Kingdom is GB). Anything that is not one of them, such as XX (no data), T1 (Tor) or garbage, is "unknown", and an unknown country is never refused.
  • The answer. A refused sign-in gets the generic message. It is logged as "Blocked login" with the reason "Sign-ins from France (FR) are not allowed" (at most once per address every 10 minutes). It is not written as a failed attempt, so it can never turn into an address block, and the allowlist wins.
  • It will not lock out the person saving it. When you save a rule that would refuse your own request, it is switched back off and Settings explains: "Country rule not saved: you are signing in from France (FR), so it would have locked you out. It has been left off." Saving a rule with no countries is switched off with "Country rule not saved: choose at least one country first. It has been left off." When your request carries no country header, the rule is saved with a warning to confirm the header and allowlist your address.
  • The way back in: wp wpus country --off, or add your address to the Allowlist.

Email sign-in code (opt-in)

This is the only feature that can refuse a sign-in whose password was correct, so it is off until you tick Ask for a sign-in code under User Security → Settings → Two-factor authentication (email codes).

Before you turn it onMake sure WordPress can send email: click Send me a test email in the same card. If mail does not work, a correct password is still refused because the code cannot be delivered. The way back in is another administrator account, or wp wpus two-factor --disable.

How it works for the person signing in

  1. They enter username and password on any sign-in form. If the password is right, the account is covered and the browser is not remembered, the sign-in is refused with "Your password was correct. Enter the sign-in code we emailed to you to finish signing in." and a six-digit code is emailed to the address on the account (subject "Your sign-in code for <site>").
  2. The form shows a Sign-in code box ("We emailed a six-digit code to the address on this account."). They enter the code (and the password again if the form cleared it) and sign in.
  3. That browser is remembered for Remember a browser for days (30) and is not asked again until the trust expires.

Who is asked

  • The roles ticked under Who is asked (administrator, editor, author and shop manager by default; the administrator role cannot be unticked). Every account that can manage_options, and every super admin, is always asked whatever its role is called. Customers and subscribers are not asked unless you tick them. The filter wpus_two_factor_applies exempts or adds one account.
  • An account with no usable email address is not challenged, because refusing it would lock the person out with no way back. The sign-in is logged ("Second factor skipped: the account has no deliverable email address.") and Settings lists those accounts.
  • Nobody is asked while the plugin steps aside for another two-factor plugin (see Requirements). Use the filter wpus_two_factor_active for protection PHP cannot see, such as host-level two-factor or single sign-on.

Which forms

Every form a person can type a password into: wp-login.php (and the interim login inside wp-admin), the WooCommerce My Account and checkout login (with its own code box), an embedded wp_login_form(), and any theme, membership or page-builder form that signs people in the normal way. A form that cannot show a code box refuses the sign-in with an extra line, "This form has no box for the code, so finish signing in at <your login URL>.", because the code belongs to the account, not to the form. REST, application passwords, XML-RPC, cron and WP-CLI have no form and are never asked.

No way round it

With Every sign-in form ticked (default), a sign-in for a covered account that would be completed without passing the check (a social-login or magic-link plugin that sets the login cookie itself, a custom form) gets no login cookie. The session WordPress just created is destroyed, the attempt is logged ("Sign-in voided: it did not go through the sign-in code check, so no login cookie was sent."), and a person in a browser is sent to wp-login.php with "For your security this account needs a sign-in code. Sign in here and we will email you one." Untick it only if such a plugin conflicts. Not affected: REST, XML-RPC, cron and WP-CLI; an administrator switching to another user; someone signed in as that user already; a sign-in straight after a password reset by email (the reset link proved the mailbox) or after registering in the same request; and a browser that has already proved the code.

Code rules

  • A code lasts Code lifetime (10 minutes). Only a hash is stored (user meta wpus_2fa_pending), with the time sent (wpus_2fa_sent) and the count of wrong tries (wpus_2fa_tries).
  • A valid code is never re-sent while it is outstanding, so repeatedly submitting the form cannot mail-bomb an address. A replacement is emailed at most once per 60 seconds (filter wpus_two_factor_resend_seconds). During that minute the person sees "A sign-in code was emailed to you a moment ago. Wait a minute, then sign in again to get a new one."
  • After Wrong codes per email (5) wrong codes the code is burnt and a new one is emailed ("Too many wrong codes. We have emailed you a new one."). An expired code is replaced the same way ("That code has expired. We have emailed you a new one."). A wrong code says "That code is not right. Check your email and try again."
  • If the email cannot be sent the sign-in is refused: "Your password was correct, but the sign-in code could not be emailed. Ask an administrator to check the site mail settings, or to turn two-factor authentication off for you."
  • A blocked address is never sent a code, and a password-reset page is never challenged. Being on the allowlist does not skip the code.
  • The code challenge is not a wrong password: it is neither recorded as a failed attempt nor scored, so typing the code wrongly cannot get an administrator blocked. Each code request is logged as a "Blocked login" event with the reason "Second factor required: a sign-in code was emailed." and therefore appears in the Dashboard's "Blocked attempts" tile.

Remembered browsers

After a correct code, the plugin sets one cookie per account, wpus_device_<user id> (HttpOnly, SameSite=Lax, Secure on HTTPS), holding a random 64-character token. The database stores only the SHA-256 hash of the token, a hash of the browser's User-Agent string, a short label (Edg, OPR, Chrome, Firefox, Safari or "Unrecognised browser"), the IP address and the dates. A database leak cannot be replayed as a cookie, and a cookie copied to another browser is useless because the token is bound to the User-Agent string. The same binding means a browser whose User-Agent string changes, for example after an update, is asked for a code again.

  • A password change or password reset forgets that user's browsers. User Security → Tools → Remembered browsers (shown while the code is on) lists every browser with Forget and Forget all. Expired entries are removed by the daily job.
  • Switching the code off in Settings does not delete remembered browsers (use Forget all first, or wp wpus two-factor --disable, which switches the code off and forgets them). Anyone signed in when you switch it on stays signed in; the code is asked at their next sign-in from an unrecognised browser.
  • If the email cannot reach someone: wp wpus two-factor --user=<login> --revoke forgets that person's browsers, and wp wpus two-factor --disable switches the feature off for everyone.

Alerts

Alerts are emails to the address in Send them to on User Security → Emails (or the site administration address when that is empty), sent with wp_mail(). All alerts need the master switch Send me email about security events and their own switch; since 1.5.0 every one of them is listed on that screen with one line that says when it is sent. Each alert type is throttled per subject (an address, an account) to one email per Repeat window (minutes) (15), so an attack never becomes one email per attempt.

Every email is a card: the plugin's shield icon (the one in the admin menu), "User Security Guard" and your site name at the top; a coloured stripe and label for how serious it is (red for an attack, amber for a warning, blue for information, green for all clear); a headline; the facts in a table (the address, account, role, form and the time); a box that says what to do next; and a button to the right screen: Blocked IPs or Blocked Users searched for the value, or the Security Logs filtered to that address or account. The footer says "This is an automated security notification from User Security Guard on <site name>." with links to the Dashboard and to the notification settings. Each email also carries a plain-text version with the same words, which mail apps that prefer text show instead. Nothing is loaded from a server: the icon travels inside the email, so nothing is blocked and nothing tracks the reader.

Two alert emails: The IP address 203.0.113.45 is blocked, with a red label, its facts and a Review blocked IP addresses button; and Someone tried to sign in to the account admin, with an amber label, the role, the form, the IP address, the risk decision and an Open the security log button
Two alerts as they arrive: an address blocked automatically (red, with when it lifts) and a wrong password on a watched account (amber). Demo data, documentation addresses.
AlertSwitch (default)Subject lineSent when
An address or network was blockednotify_ip_blocked (on)IP address blocked: <address> (or Network blocked: <network>, Range blocked: <range>)A new IP, network or range block is created, automatic or manual. Says which, and the reason; for a network or range it says that nobody in it can sign in and how many addresses it covers, and for a block that lifts by itself it says when.
An account is attacked from several addressesnotify_account_targeted (on)Account blocked: <name> and Account targeted from many IP addresses: <name>An account block is created (one email for the whole account, not one per row), or a distributed attack is detected. For an administrator account the text says so.
Somebody tries the password of a watched accountnotify_admin_failed_logins (on)Someone tried to sign in to the account "<name>"A wrong password on an account in a watched role. Lists the account and its roles, where (the sign-in page, wp-admin, the WooCommerce form, XML-RPC, REST, or another form), the IP address, the risk decision and the reasons.
(same switch)notify_admin_failed_logins (on)Administrator block refused: <identifier>An account block was refused by administrator lockout protection (or because account blocking is off). Tells you to block the IP address instead.
Right password, wrong sign-in codenotify_two_factor_failed (on)Sign-in code problem on the account "<name>"A wrong code, a burnt code, a sign-in that skipped the code, or a code that could not be emailed. An expired code or a "wait a minute" is not an alert.
A watched account signs innotify_privileged_signin (off)The account "<name>" signed inA completed interactive sign-in by a watched account (not one the code check voided). Includes address and browser.
A burst of traffic on the sign-in pagenotify_attack_spike (on)Login endpoint attack spike from <address>An address reaches the high-frequency rate (see Bot detection).
XML-RPC is being hammerednotify_xmlrpc_attack (on)Blocked XML-RPC request from <address>A request to XML-RPC is refused while XML-RPC is blocked. One email per repeat window for the whole site, whichever address it came from, naming the latest address and how many requests were held back since the last email.
A sign-up was refusednotify_registration_refused (off)Sign-up refused: <reason>Sign-up protection refused a sign-up (1.7.0). One email per repeat window for the whole site, naming the form, the reason, the address and the email domain, and how many refusals were held back since the last email. Off by default: a wave of bots is many refusals, and the digest counts them anyway.

Accounts to watch chooses the roles behind the three "watched account" alerts: administrator, editor, author and shop manager by default. Administrators and any account that can manage_options are always watched. On the Emails screen each role is a card showing what the role can do, with a "Recommended" tag on the roles that can write or run the shop, a live count, and the quick picks Select recommended roles and Administrators only. A role that does not exist on the site (shop manager without WooCommerce) is never matched.

Security digest

One email (the same card as the alerts, coloured by the threat level), Every day, Every week (default) or Every month, set with Send a digest under User Security → Emails (Never switches it off). It follows the master switch Send me email about security events and goes to the address in Send them to. It is a calm summary and also proves the protection is running when nothing happened.

  • Subject: "[<site>] Security digest: N failed sign-ins, threat level Low/Medium/High".
  • Body: the threat level as the headline, with one sentence about it; the period, its length and the successful sign-ins (only when logging and "Successful logins" are on); four tiles with the failed sign-ins, the requests refused because the address or account was already blocked, the different IP addresses involved and the accounts that exist on the site and were targeted; IP addresses and accounts blocked in the period and what is blocked right now; automation detections and refused XML-RPC requests (only when there were any); the five busiest IP addresses and the five most-targeted accounts as ranked lists; while Sign-up protection is on (or when anything happened), a Sign-ups section with the accepted and refused sign-ups, the three most common reasons and the new accounts blocked for probing XML-RPC (1.7.0; the sign-in figures above leave sign-ups out); and, when nobody failed to sign in, the line "A quiet period: nobody tried and failed to sign in. The protection was running the whole time." A button opens the Dashboard.
  • Threat level is judged per day, so a monthly digest is not "High" merely because a month is long: High from 200 failed or refused attempts a day or 10 new blocks a day; Medium from 30 attempts or 2 new blocks a day; otherwise Low. The filter wpus_digest_level changes it.
  • It never mails out of the blue. The daily maintenance job starts a clock the first time it sees the digest switched on; the first email arrives one full period later (the job treats a digest as due six hours before a full period has passed). Switching the digest off, or email notifications off, makes the next daily run forget the clock, so switching it on again starts a fresh period. A period is 1 day (daily), 7 days (weekly) or 30 days (monthly).
  • A failed send is retried by the next daily run. A site that was off for months reports at most 35 days.
  • Attempt rows are kept only for Keep attempt rows for days (30), independently of the digest period. When the period is longer, the email says the attempt figures cover only the most recent part and does not claim a quiet period.
  • Send one now: User Security → Tools → Security digest (Send a digest now, 1 to 35 days, default 7), or wp wpus digest --send [--days=N]. wp wpus digest prints it. Neither changes the schedule. The digest needs WP-Cron.
  • The filter wpus_email_message (context digest) changes the email. The older wpus_digest_lines is deprecated since 1.3.0 but still honoured: lines it changes replace the digest body inside the card.
The weekly security digest email with Threat level High, the period, four number tiles, the blocking summary, other signals, the five busiest IP addresses and the most targeted accounts; next to it the sign-in code email with the six-digit code in a large box, the account, IP address, browser and time, and a Not you? box
Left: a busy weekly digest (red for High). Right: the sign-in code email a person gets, with the code large and easy to copy. Demo data.

Security log

The log has 23 event types, grouped into six kinds for the one-click filters (sign-ins, blocks, XML-RPC, bots and probes, sign-ups, administration). Each is controlled by a switch under User Security → Settings → Logging; the events that record an administrator's decision are always written while Logging is on, because they are the ones an audit needs.

Event (stored value)LabelWritten whenSwitch
login_successSuccessful loginA non-administrator signs in (also with an application password).Successful logins
login_failedFailed loginA wrong password for a non-administrator or unknown account.Failed logins
login_blockedBlocked loginA request refused because the address or account is blocked, a country refused, XML-RPC authentication is off, an early refusal, or a sign-in-code challenge.Requests refused by a block
ip_blockedIP blockedA new block of an address, a network or a range (automatic or manual).Requests refused by a block
user_blockedUser blockedA new account block.Requests refused by a block
block_deletedBlock record deletedAn administrator uses Delete on a block.Requests refused by a block
suspicious_activitySuspicious authentication activityA suspicious result, a refused account block, the first exploit probe in a window, or a network or range that attacked but was left alone on purpose (at most once an hour per network).Requests refused by a block
bot_detectedBot / automation detectedA failed attempt with bot signals, or a burst on the login page.Bot detections
xmlrpc_requestXML-RPC requestAn allowed XML-RPC call (method name only).XML-RPC requests (and Log XML-RPC activity)
xmlrpc_blockedXML-RPC blockedA request refused while XML-RPC is blocked.XML-RPC requests
registrationSign-upAn account was created through the My Account or wp-login.php registration form (1.7.0).Log sign-ups (Sign-up protection)
registration_blockedSign-up refusedSign-up protection refused a sign-up; the reason names the check (1.7.0).Requests refused by a block
user_marked_fakeAccount marked as fakeAn administrator marked an account as fake: what was learned, then the account was deleted (1.7.0).Always
admin_loginAdministrator loginAn account that can manage_options signs in.Log successful admin logins
admin_login_failedAdministrator login failureA wrong password for an administrator account.Log failed admin logins
ip_unblockedIP unblocked (manual)An address is unblocked by a person, WP-CLI or automatic release.Always
user_unblockedUser unblocked (manual)An account is unblocked.Always
settings_changedSecurity settings changedSettings saved or imported, every manual action on the admin screens, and every export.Always
allowlist_added, allowlist_removedAllowlist entry added, Allowlist entry removedAny change to the allowlist, including automatic entries.Always
logs_purgedSecurity log cleaned upRetention cleanup, a manual purge, or a privacy erasure.Always
emergency_enabled, emergency_disabledEmergency mode enabled, Emergency mode disabledEmergency mode is switched.Always
  • What a row holds: time, event, risk level (Low, Medium, High, Critical) and score, IP address (or the network or range), user ID, username, email, endpoint, request method, user agent (first 191 characters), result (Allowed, Blocked, Failed, Success, Warning), the reason, an administrator flag and a small JSON context. For a failed sign-in with a name that is not an account, the context also holds the name that was typed (see below).
  • Names typed in failed logins (Settings → Logging, on by default). The username or email typed into a failed sign-in is kept in the row when it is not an account on your site, so you can see what attackers are guessing. It is lower-cased, cut to 100 characters, shown as plain text, searchable and included in the CSV export (column attempted_name). It is never copied into the attempts table, so it can never feed a counter. A visitor can type anything into the username box, including a password, so untick the switch if you do not want text typed by visitors kept; names already stored stay until log retention removes them.
  • Redaction. Passwords, cookies, tokens and request bodies are never stored. In a context, any key whose name contains pass, pwd, secret, token, authorization, cookie, session, nonce, api_key, bearer or credential is replaced by "[redacted]". Values are cut to 200 characters, a context holds at most 25 entries and three levels, and non-scalar values become "[unsupported]". The filter wpus_log_data changes a row just before it is written.
  • Retention. Keep log rows for 30, 60, 90 (default), 180 days, 1 year or Forever. A daily job deletes older rows, 1,000 at a time and at most 20,000 per table per run, and writes a "Security log cleaned up" entry. Blocks are never touched by retention.
  • Switching Logging off stops all logging, including the administrator-decision events. Blocking, scoring and the digest's attempt figures do not depend on it.

Emergency mode

One switch on User Security → Tools (Turn emergency mode on, with a confirmation) tightens everything at once during an attack. It is never switched on automatically and it only tightens. Your saved values are untouched: Settings keeps showing them, and they apply again when you switch it off. While it is on, a red notice appears on every plugin screen and a chip "Emergency mode" in the header.

SettingValue while emergency mode is on
Suspicious attempts per IP1
Block after failed attempts from one IP2
Block after failed attempts on one username or one email3
Block an account attacked from this many IPs3
Failed attempts before an admin account is treated as targeted1
Risk score for "block" and for "suspicious"50 and 10
Requests per minute that count as high frequency5, or your value if lower
Counting window (minutes)at least 60
Default usernames threshold, exploit probing threshold2 and 3, or your values if lower; both rules forced on
Rotating-address protection and the wide-range ruleforced on; the network number is capped at 5, so a /24 blocks at 5 addresses and a /20 at 13. The widest range (/16) is not switched on
Login protection, administrator protection, bot detection, WooCommerce protectionforced on
XML-RPCblocked, authentication not allowed
Logging, "Failed logins", "Requests refused by a block", "Bot detections", "XML-RPC requests", "Names typed in failed logins", email notificationsforced on
Sign-up protectionevery check forced on; minimum fill time doubled (at least 4 seconds); sign-ups per address halved (no limit becomes 2); the browser limit 24 versions at most; the XML-RPC watch 60 minutes at least
Trust the address administrators sign in frompaused
Automatic release of blockspaused: blocks never lapse and nothing is released

Emergency mode does not change Automatically block administrator accounts (the Tools card says administrator account blocking "is allowed" while it is on, but the saved setting still decides), the country rule, the email sign-in code or "Successful logins" logging.

Reverse proxies and the visitor's address

By default the plugin uses REMOTE_ADDR, the address of the connection. Behind a CDN or reverse proxy every visitor then arrives from the proxy's address, so all failures pile up on one address. Turn on Trust proxy headers and choose the header your proxy sets (X-Forwarded-For (default), CF-Connecting-IP, X-Real-IP or True-Client-IP) so the plugin sees the visitor. It is off by default because those headers are trivially faked when no proxy overwrites them, and a faked address would move the counters elsewhere.

  • When the chosen header holds a comma-separated list, the plugin uses its last entry only: a client can prepend fake entries, but the entry your proxy appended sits at the end. A port after the address ("198.51.100.7:443") is ignored. If the last entry cannot be read (for example "unknown"), an earlier entry is not used instead, because the visitor wrote it.
  • When the header is missing, the connection address is used. When the header is on the request but its last entry is unusable, the connection address is used too and the request is not treated as one identifiable client (it is the proxy, not a person), so the rules that block or trust a single client stay quiet.
  • "One identifiable client". The rules that are cheap for a stranger to trigger (exploit probing, repeated default usernames) and the rule that trusts an address (administrator sign-in) act only on an address that is valid, public (not private, loopback, reserved, carrier-grade NAT, documentation or multicast space) and, while Trust proxy headers is off, not accompanied by a forwarding header (X-Forwarded-For, Forwarded, CF-Connecting-IP, True-Client-IP, X-Real-IP or X-Cluster-Client-IP). Behind a proxy that the plugin was not told about, those rules stay quiet on purpose instead of blocking everyone behind the edge. On a private network that you know is not shared (a lab, local development), return true from wpus_attributable_ip.
  • Settings shows "Your site appears to be behind a proxy or CDN: every request arrives from the same address. Without this setting, all traffic is counted as one IP." when a reverse-DNS lookup of the address of the administrator viewing the screen returns a host name containing cloudflare, amazon or googleusercontent (see Privacy). It is a hint; User Security → Tools → Sign-in diagnostics is the reliable check.

Sign-in diagnostics

User Security → Tools → Sign-in diagnostics has two read-only checks that run only when you open the screen (or run wp wpus diagnose), never on a visitor's request.

  • Which address does the plugin see for you? Shows the connection address, the address the plugin uses (with "One identifiable client" or "Not one client"), whether proxy trust is on, and every forwarding header your request carried with the address it would give and a "Cannot be selected" marker for Forwarded and X-Cluster-Client-IP. One sentence tells you what to do: that the connection address is correct for a site served directly; that the request passed through a proxy but trust is off and which header to choose (edge headers such as CF-Connecting-IP, True-Client-IP and X-Real-IP are suggested before X-Forwarded-For, which a client can add to); that the header is trusted but missing or invalid; or "Working: the <header> header is trusted and gives the visitor's address (<address>) instead of the proxy's (<address>)." It describes your request only. Open Tools through the same route your visitors use.
  • Other plugins on the sign-in path. Lists every callback on authenticate and wp_authenticate_user with its priority and where it comes from (WordPress core, this plugin, a named plugin, a must-use plugin, the theme). A callback from outside that runs after priority 100, where this plugin gives its final answer, is marked "After our final check", because it could overrule a refusal. It is a hint, not a verdict. A known login limiter that is active is named in a warning here and at the top of Settings.

Export and import

On User Security → Tools → Export and import you can download the security data and move a configuration between sites.

  • Downloads. Security log (CSV) (with an extra Last 30 days button; its last column, attempted_name, holds the name typed into a failed sign-in), Blocked IPs and accounts, with their history (CSV), Allowlist (CSV) and Settings (JSON). File names are wpus-<what>-<date>.csv or .json. Every download needs the managing capability and a valid link, and is itself written to the log ("Exported: <name>").
  • Row limit. A log or block CSV holds at most 100,000 rows, oldest first (the filter wpus_export_row_cap changes it). When more exist, the last line says "# Export stopped at the row limit (100,000 rows); newer rows are not in this file. Download a shorter date range for the rest." The log is read in pages of 1,000 by row ID, so an attack in progress cannot repeat or skip rows.
  • Spreadsheet safety. A cell that starts with =, +, -, @, a tab or a carriage return gets a leading apostrophe, because usernames, user agents and reasons come from whoever tried to sign in.
  • The settings file is JSON with plugin, format (1), version, exported_at and settings (this plugin's own settings only).
  • Import settings. Paste the JSON into the box and click Import settings (or run wp wpus import-settings <file>). A file larger than 200,000 bytes, not valid JSON, not from this plugin or made by a newer format is refused with a message and nothing is imported. Values go through the same checks as the Settings screen; unknown keys and non-scalar values are ignored, and a file that names only some settings leaves the rest alone. The result says how many settings changed and names them (up to 12), and names every protection the file switched off (login protection, rotating-address and range blocking, account blocking, bot detection, administrator protection, XML-RPC blocking, default usernames, exploit probing, WooCommerce protection, repeat offenders, logging, email notifications).
  • Thirteen settings are never imported: trust_proxy_headers, proxy_header, notify_email, two_factor_enabled, emergency_mode, delete_data_on_uninstall, admin_account_block, country_mode, country_header, country_codes, trust_admin_ips, block_auto_release_hours and block_escalation_factor. They describe the server, can lock someone out, or loosen a protection you chose to switch on.
  • The lists (log, blocks, allowlist) are export-only: an import is not a second way to create blocks that the risk engine never decided on.

Privacy tools

  • Sign-in notice (opt-in). Sign-in notice under User Security → Settings → Privacy prints a short line on the WordPress sign-in form (between the password field and the Remember Me checkbox in WordPress 7.1.2, and in the interim login) and at the end of the WooCommerce My Account and checkout login forms. Default wording: "To keep this site secure, your IP address and browser details are recorded when you sign in. {privacy_policy}". Notice text replaces it: up to 600 characters; only links, bold, italics and line breaks are kept (cleaned when saved and again when shown). {privacy_policy} becomes a "Privacy policy" link to your privacy policy page when WordPress has one, and nothing when it has not. It is printed by the server with no script and no cookie, and it does not appear on a form that never fires login_form, such as a fully custom AJAX form or wp_login_form().
  • Suggested privacy-policy text appears in Settings → Privacy → Policy Guide under "Security log". An admin notice on every admin screen reminds users who can manage the plugin to publish a privacy policy while the log has rows and no policy page is published.
  • Personal-data exporter and eraser ("Security log") appear under Tools → Export Personal Data and Tools → Erase Personal Data. They look up rows by email address, 500 per batch. The exporter returns log rows (event, date, IP address, username, email, endpoint, outcome, reason, risk) and attempt rows (date, IP address, username, endpoint, outcome, reason). The eraser deletes the log rows and, on its first pass, the attempt rows for that email, and writes a "Privacy erasure request" log entry that does not contain the email address. Blocks and allowlist entries are not erased: a block is a security decision, and lifting one silently would break the rule that only an administrator ends a block. The filter wpus_erase_attempts (default true) lets you stop erasure from clearing attempt rows, which are the counters behind the limits.
  • Sign-up record (1.7.0): the export of a person also holds the sign-up record of their account ("Sign-up record": when, from which address and browser, which form), and erasing removes it.

6. Admin screens

The plugin adds one top-level menu, User Security (right after Users, a shield icon), with eight screens. Every screen needs the capability wpus_manage_security (administrators have it) and shares one header: a plain title with a muted line under it, status chips on the right ("Version 1.3.1", plus "Emergency mode", "Two-factor on", "Country rule: block FR" or "only" and "Auto-release 24h" when those are active) and a row of pill tabs with live counts on the two block lists. The browser page title of the screens reads "WP User Security", the plugin's earlier name.

ScreenPathWhat it is for
DashboardUser Security → DashboardStatus tiles, the summary of what the recorded attempts show, a 14-day chart, the latest sign-ins, the top countries and the top blocked IPs, the latest events and administrator activity.
Security LogsUser Security → Security LogsEvery recorded event, filterable.
Blocked IPsUser Security → Blocked IPsIP, network and range blocks, with a manual block form.
Blocked UsersUser Security → Blocked UsersAccount blocks, with a manual block form.
AllowlistUser Security → AllowlistTrusted addresses and ranges.
EmailsUser Security → EmailsEvery email the plugin can send, with a switch for each (1.5.0).
SettingsUser Security → SettingsEvery other setting, in cards.
ToolsUser Security → ToolsEmergency mode, retention, diagnostics, digest, export and import, system information, remembered browsers.
Users (WordPress)Users → All UsersA Mark as fake link under each account and a bulk action of the same name (1.7.0). See Users screen.

Dashboard

Titled "Security Dashboard". A switch at the top chooses the period for the event counters: Last 24 hours (the default), Last 7 days, Last 30 days, Last year and All time (?wpus_period=24h|7d|30d|365d|all; an unknown value falls back to 24 hours). The eight tiles are links:

TileCountsOpens
Blocked IPsIP blocks that are active right now (not limited by the period).Blocked IPs
Blocked usersAccounts that cannot sign in right now, each counted once (not limited by the period).Blocked Users
Failed logins"Failed login" events in the period (wrong passwords on non-administrator or unknown accounts).Security Logs, result Failed
Successful logins"Successful login" events in the period (non-administrators).Security Logs, event Successful login
Blocked attempts"Blocked login" events in the period, described as "Rejected by an active block". This includes refused countries, XML-RPC authentication that is off, early refusals and sign-in-code challenges. Refused sign-ups are not in the number: when there were any in the period, the line under the tile adds "Plus N refused sign-ups" (1.7.0).Security Logs, result Blocked
Bots detected"Bot / automation detected" events.Security Logs, event Bot / automation detected
XML-RPC requests"XML-RPC blocked" events (requests refused).Security Logs, event XML-RPC blocked
Admin failures"Administrator login failure" events.Security Logs, administrator activity only

Below the tiles:

  • What the recorded attempts show: the card that says in plain words whether an attack is going on and why nothing has been blocked yet, with the networks that attacked from many addresses and a one-click Block network or Block range. See What the recorded attempts show.
  • The last 14 days: stacked bars of successful, failed and blocked sign-in events per day, built from the log (so it is empty when logging is off). A visually hidden table carries the same numbers for screen readers. With no activity it says "No sign-in activity has been recorded in this window yet."
  • Login attempts, Top countries and Top IPs blocked: three cards between the chart and the latest events (1.6.0). See Who is knocking.
  • Latest security events: the ten newest log rows (When, Event, Account / IP, Result) and a button Open the full log.
  • Administrator activity: administrators on the site; failed attempts, successful administrator logins and "Rejected while an admin account was blocked" for the period (this last figure counts all "Blocked login" events in the period, not only those of administrators); and Admin accounts auto-blocked: "Never (recommended)" or "Enabled". The safety warnings (single administrator, non-empty allowlist, automatic administrator blocking on, XML-RPC allowed) appear here.
  • Where to go next: links to the block lists, the allowlist, Tools and Settings.
The Security Dashboard: eight tiles, the card What the recorded attempts show with its network table, the 14-day chart, the latest events and administrator activity
The Dashboard with demo data: the tiles, the summary card (a /20 range blocked by itself, a /24 and an IPv6 /64 network, and a range that is not blocked yet with its Block range link), the 14-day chart and the latest events.

Security Logs

Titled "Security Logs", subtitle "Every authentication attempt, block and setting change this plugin has recorded." When the outcome filter is All, Failed, Blocked or Warning, the card What the recorded attempts show sits at the top. Under it come one-click outcome links with the number of rows behind each (All, Allowed, Blocked, Failed, Success, Warning; counted over all time, whatever the other filters say). Since 1.4.0 a second row of links filters by kind (Every kind, Sign-ins, Blocks, XML-RPC, Bots and probes, Sign-ups, Administration), each with its count, so refused XML-RPC probes can be set aside from the sign-ins of real people. When a distributed sign-in attack is on, the card opens with a red Under attack line that says how many addresses and until when.

Filters: a search box ("Search reason or agent"; it searches the reason, username, email, IP address, user agent and the name that was typed), IP address or network (one address, or a network or range such as 203.0.113.0/24 or 178.239.192.0/20: it matches that network's members exactly, edges included), Username, Email, From date, To date, Event type (all 20 events), Risk level (Low, Medium, High, Critical), Result (Allowed, Blocked, Failed, Success, Warning) and the checkbox Administrator activity only. Buttons: Filter logs and Reset. The table shows 25 rows per page, newest first:

  • When: the day and the time on two lines, with the endpoint under them (wp-login, xmlrpc, rest, wp-admin, woocommerce).
  • Event: the event and, under it, the client as a plain label ("Chrome on Windows", "curl", "Python requests"). A client that claimed to be a search-engine crawler gets a pill: Googlebot (verified) in green, or claims to be Googlebot (not verified: it is not one) in amber (1.4.0).
  • Account: the username or email, an admin mark for administrators, a no such account mark when the name that was typed belongs to nobody on your site, and the email underneath. Under an account created through a registration form (a "Sign-up" row) a Mark as fake link opens the confirmation on the Users screen (1.7.0).
  • IP address: the address in a monospace chip. A refused row carries a Block address link (the ordinary audited block action, with a confirmation), and an address that is already blocked is marked Blocked, Network blocked or Range blocked.
  • Reason: the reason text, or for a failed sign-in the error as a label ("unknown username", "unknown email", "wrong password", "refused (blocked)", "form not loaded"), and a detail link that opens a panel of labelled facts: the name typed in the sign-in form, what the counters were at that moment next to the limits ("From this address: 10 failed sign-ins (blocked at 10)"), the rules that fired, the network, the request and the user agent. It answers "why was this not blocked?" without showing stored JSON.
  • A Sign-up refused row says which check refused it ("Sign-up refused: hidden field filled in"); a short label is added only when the text does not already name the check, and the detail adds the fact Sign-up check (1.7.0).
  • Risk and Outcome.

When, Event, IP address and Risk are sortable. The bulk action Delete selected log rows deletes only log rows (never blocks). A note under the table says how long rows are kept.

The Security Logs: the summary card, outcome links with counts, filters and compact rows with client labels, no such account marks and Range blocked marks
Security Logs with demo data: outcome links with counts, filters and compact rows. A hopping bot's addresses are marked Range blocked; a refused address that is not blocked yet offers Block address.

Blocked IPs and Blocked Users

The two screens share one layout. A notice says how blocks end on this site, and the subtitle changes with it: "A block stays until you remove it here — nothing lifts it automatically." when nothing lifts a block; "IP addresses and networks that cannot sign in. A block stays until you remove it here — except a wide range that the plugin blocked by itself, which lifts after 48 hours." on Blocked IPs with the default wide-range rule; "Automatic blocks lift by themselves after a while; blocks you make by hand and permanent blocks stay until you remove them." when automatic release is on. Filters: a search box (address; or username or email; it matches the value, username, email and reason), Status (Active blocks (default), Blocked, Permanent, Unblocked (history), Any status), Period (on the date the block started) and Filter and Reset. 20 rows per page.

  • Blocked IPs columns: IP address (with "last tried: <username or email>" underneath, and a network mark for a /24 or /64, a range /20 mark for a wider range; hover for the number of addresses it covers), Status (with a "lifts in 2 days" countdown for a block that will lift), Reason, Attempts, Usernames tried (for a network or range: how many different addresses were seen, "36 addresses"), Last detected, Blocked. The manual form is titled "Block an IP address or a network manually".
  • Blocked Users columns: Account (username, with the email underneath), Status, Reason, User ID ("no matching user" when none), Distinct IPs, Last detected, Blocked. Blocking a real account writes up to three rows (the username, the email address and the user ID, so the account cannot come back through another sign-in method): every row is listed, while the tab and the Dashboard count the account once.
  • Status, address or account and "Blocked" date are sortable. Row actions, bulk actions and the manual block forms are described under Blocks. The "Blocked" column also shows "unblocked <date>" for a history row.
Blocked IPs: a wide range with its range /20 mark and a countdown, networks with their network mark, single addresses with their reasons and a permanent block
Blocked IPs with demo data. The first row is a /20 range the plugin blocked by itself (note the countdown); the next are a /24 network and an IPv6 /64 network, then single addresses blocked by their own limits, one permanent block and one made by hand.
Blocked Users: a contractor account blocked by hand and an account blocked after a distributed attack, one row for each identifier
Blocked Users with demo data: an account blocked by hand, and a real account blocked after an attack from 12 addresses (three rows: username, email address and user ID).

Allowlist

Titled "Allowlist", with the warning "An allowlisted address bypasses all login protection." The left card, Add an address, takes IP address or range and an optional Label and shows "You are browsing from <your address>." The right card lists Allowlisted addresses (N) with columns Address or range, Type (Address or Range), Label, Added (date and who, plus "Trusted until <date>" or "Expired <date>" for automatic entries) and a Remove action with a confirmation. See Allowlist for how entries behave.

The Allowlist: an office address, an office VPN range and an automatic entry added after an administrator signed in
The Allowlist with demo data: two entries added by hand and one added automatically after an administrator signed in (it expires).

Emails

Titled "Emails", subtitle "Every email this plugin can send, who receives it and a switch for each. Security alerts and the digest go to you; sign-in codes go to the person signing in." Added in 1.5.0 so the email switches are no longer one card among twenty on Settings. One form, three cards:

  • Security alerts: the master switch Send me email about security events (off: no alert and no digest is sent, whatever is ticked below; the sign-in code is not affected), Send them to (empty = the site administration address; the line under it says where they go right now), the list Tell me when with one switch and one explanatory line per alert (an address or network was blocked, an account is attacked from several addresses, somebody tries the password of a watched account, right password but wrong sign-in code, a watched account signs in, a burst of traffic on the sign-in page, a distributed sign-in attack starts, XML-RPC is being hammered, a sign-up was refused), At most one email per alert every N minutes (the repeat window) and Accounts to watch (the role cards). With the master switch off the list is dimmed, not hidden.
  • Security digest: Send a digest (Never, Every day, Every week, Every month), the last and next digest when the clock has started, and Send a digest now.
  • Emails to the people who sign in (no switches): the sign-in code with an On/Off pill and a link to its switch under Settings → Two-factor authentication, and the Send me a test email button.

The form writes only the email settings (the Settings form writes everything else), so saving one screen never changes the other. The links at the foot of every alert email ("Choose which emails you get") open this screen; the Notifications card on Settings points here too.

The Emails screen: the master switch, the address, nine alerts each with a switch and one explanatory line, the repeat window, the role cards of Accounts to watch, and the Security digest card with its schedule and last and next digest
The Emails screen: every email with a switch and one line that says when it is sent; the digest card below shows when the last one went out and when the next is due.

Settings

One page, one form, a series of cards in this order: Login protection, Administrator protection, Bot detection, WooCommerce sign-in, Default usernames, Exploit probing (404 requests), Sign-in form check, Sign-up protection, Distributed attacks, Account discovery, Search-engine crawlers, XML-RPC, Blocking behaviour, Country rule, Logging, Notifications (since 1.5.0 a pointer to the Emails screen), Reverse proxies, Two-factor authentication (email codes) and Privacy. A section navigation built from the card headings and a sticky Save settings bar ("Changes take effect as soon as you save.") make it easy to move around. Saving shows a green "Settings saved." together with anything the plugin refused (for example a country rule that would have locked you out). Below the form, What this plugin detected lists WordPress version, site URL, timezone, PHP version, memory limit, WP cron, object cache, database schema, plugin version, XML-RPC and proxy-header trust, with a link to the full system information on Tools. Warnings at the top come from the same checks as Tools and from the safety warnings. Every setting is explained under Settings.

Settings, Login protection: the limits, the rotating-address rule, the wide-range rule and the release of a wide-range block
Settings → Login protection with the defaults: the per-address limit that blocks, the network rule (10 different addresses), the wide-range rule (on) and the widest range (off), and the 48-hour release of a wide-range block.

While emergency mode is on, a notice says "Emergency mode is on, so the values below are overridden with stricter ones. Turn it off on the Tools screen to edit them again." The form always shows what you saved, so saving never stores the emergency values.

Tools

Titled "Tools", subtitle "Emergency mode, data retention and — if you ever lock yourself out — the way back in." Top warnings: HTTPS missing, tables not installed, WP cron disabled or overdue, a custom-login-URL plugin ("Bot detection keys on the login endpoint, so frequency counting may follow a different path than expected."), another login-limiting plugin, and callbacks after this plugin's final check. Cards, in order:

Tools: emergency mode, log retention, the way back in after a lockout and what the plugin stores
Tools: emergency mode, log retention, the three ways back in after a lockout and what the plugin stores. Below them come the sign-in diagnostics, the digest, export and import and the system information.
  • Emergency mode: Turn emergency mode on (with a confirmation) or Turn emergency mode off. See Emergency mode.
  • Log retention: the number of log rows and how long they are kept, the oldest row, the number of attempt rows; Delete log rows by age (Older than (days), default 90, 1 to 3,650 in the form, Delete those log rows); and Run the daily cleanup now (Apply the retention settings now). Cleanup only deletes log and attempt rows.
  • Clear the attempt counters: Delete all attempt rows restarts every failure counter from zero. Existing blocks stay. Occasionally useful after a false alarm, a bad idea during a real attack.
  • If you are locked out: the three ways back in (the WordPress recovery email, an allowlist entry from another administrator session, and WP-CLI with wp wpus unblock --account=YOUR_USERNAME, wp wpus unblock --ip=203.0.113.10, wp wpus blocks and, when the code is on, wp wpus two-factor --disable).
  • What this plugin stores: counts of blocked IPs, blocked accounts, allowlisted addresses and (with the code on) remembered browsers, and the statement that passwords, cookies, authentication tokens, nonces and request bodies are never stored.
  • Remembered browsers (only while the email code is on): user, browser, trusted since, last used, expires, Forget per row and Forget all.
  • Sign-in diagnostics: see Sign-in diagnostics.
  • Security digest: schedule, last and next digest and recipient, and Send a digest now with Cover the last (days) (default 7, 1 to 35).
  • Export and import: see Export and import.
  • System information: groups WordPress (version, multisite, site URL, login endpoint, admin language, timezone), PHP (version, memory limit, max_input_vars), Environment (persistent object cache, WP cron, plugin memory in use), Plugin data (database schema, plugin version, managing capability, blocked IPs and accounts) and Posture (XML-RPC, proxy header trust, custom login URL plugin, other login-limiting plugins, WooCommerce sign-in form). A "Check" flag marks anything worth a look.

Users screen: Mark as fake

Since 1.7.0 WordPress's own Users → All Users list has a Mark as fake link under each account and a bulk action of the same name (for people who can manage the plugin). Neither changes anything by itself: both open a confirmation panel at the top of the Users screen (users.php?wpus_fake_confirm=<ids>) that lists each account with when it registered, the address it signed up from and its browser, and says Will be marked or why it cannot be (your own account, a watched role, orders, content). The one button, "Mark N accounts as fake and delete them", does the work; Cancel goes back. What it does is described under Mark as fake. On a phone each account is shown as labelled lines.

On a phone

Since 1.5.0 every screen is laid out for a phone as well: the tab bar wraps into rows (it used to scroll sideways and hide the current tab), the dashboard tiles sit two to a row, the compact tables of the Dashboard and of the activity summary stack each row as labelled lines instead of scrolling sideways inside the card, the three cards of 1.6.0 show every row as two short lines, and on Settings a Jump to a section select takes the place of the section list. The list tables (Security Logs, Blocked IPs, Blocked Users, Allowlist) use WordPress's own phone layout: the first column is shown and the arrow at the right of a row opens the rest. Only the 14-day chart still scrolls sideways, on purpose, because its labels would be unreadable any smaller.

Three phone-width views side by side: the Dashboard with wrapped tabs and two tiles per row, the Emails screen with its switches, and Settings with the Jump to a section select
The Dashboard, the Emails screen and Settings at phone width (390 pixels): wrapped tabs, two tiles per row, switches that stay tappable, and the jump-to-section select.

WordPress Dashboard widget

Users with the managing capability see a widget titled "WP User Security" on Dashboard → Home. It lists Blocked IPs, Blocked users, Failed logins (24h), Blocked attempts (24h) and Admin failures (24h), and a link Open the security dashboard.

7. Settings

All settings live in one option, wpus_settings (104 keys), saved from User Security → Settings through the WordPress Settings API (the form posts to options.php). Tick boxes are stored as 1 or 0. A number outside its range is moved to the nearest limit when you save, and an empty or non-numeric number becomes the lowest allowed value. Unknown keys that another plugin added to the option are kept. Anything that is not a whitelisted choice (the proxy header, the country header, the digest schedule, the log retention) falls back to its default.

The browser form is stricter than the saved range for four settings: Suspicious attempts per IP accepts 1 to 500 in the form, Counting window (minutes) 5 to 10,080, Failed attempts before an admin account is treated as targeted 1 to 100, and Requests per minute that count as high frequency 1 to 600. The tables below give the saved range, which also applies to values from an import, wpus_settings_sanitized and WP-CLI.

The filter wpus_settings can force values from code. The Settings screen shows what you saved, not what the filter or emergency mode forced.

Login protection

The limits that decide when traffic looks suspicious or is blocked. How they combine is explained under How a sign-in attempt is judged. Settings shows the current meaning in one sentence: "Currently: an address becomes suspicious after 5 failed attempts within 60 minutes and is blocked at 10. An account has its own limit, and the risk score (below) can block earlier when several signals add up."

Setting (label)KeyDefaultWhat it does
Login protection ("Score failed logins and apply blocks")login_protection_enabledonMaster switch for scoring, recording failures, blocking at sign-in, early refusal and the generic error messages.
Suspicious attempts per IPsuspicious_failed_per_ip5Failed sign-ins from one address (counting the attempt being judged) that make it "suspicious" (adds 25 points and 20 for the account). Saved 1 to 1,000. Never blocks on its own. It is also the "suspicious" limit for accounts.
Block after failed attempts from one IPmax_failed_per_ip10Failed sign-ins from one address inside the counting window that block the address, counting the attempt being judged (the 10th blocks). Saved 1 to 1,000. A request that was turned away because an account or a network was already blocked does not count. Behind a proxy or CDN that is not set up under Reverse proxies the address is not one visitor, so there it is only scored. XML-RPC uses the lower of this and its own limit.
Block after failed attempts on one usernamemax_failed_per_username20Failures against one username that ask for an account block. Saved 1 to 1,000.
Block after failed attempts on one emailmax_failed_per_email20The same for an email address. For ordinary accounts the higher of the username and email limits applies. Saved 1 to 1,000.
Block an account attacked from this many IPsunique_ip_threshold10Distinct addresses with failures against one account that mark a distributed attack (70 points, asks for an account block). Saved 1 to 1,000.
Block a network that attacks from many addresses (switch)network_block_enabledonThe rotating-address rule: when many different addresses of one network (an IPv4 /24, an IPv6 /64) are refused or try names that are not accounts, the whole network is blocked. Needs Login protection on. The wide-range rule below needs it too. See Rotating addresses and ranges.
different addresses within the counting windownetwork_block_threshold10How many different addresses make a network hostile. Saved 3 to 500. A /20 needs 2.5 times this (25) and a /16 six times (60). Emergency mode caps it at 5.
Also block the wider range (an IPv4 /20, an IPv6 /48)network_block_wideonBlocks the range around networks when addresses spread over at least three of its networks attack.
Also watch the widest range (an IPv4 /16, an IPv6 /32)network_block_widestoffOff unless you choose it: a /16 can be a whole provider. Needs six times the network number (60) over at least three of its /20 ranges.
Release a wide-range block by itself after (hours)network_wide_release_hours48A range (a /20 or /16, an IPv6 /48 or /32) that the plugin blocked lifts after this many hours, even when automatic release is off. 0 keeps it until you unblock it. A blank or invalid value keeps the stored one. Saved 0 to 8,760. A block you make by hand never expires.
Account blocking ("Block accounts that cross a threshold")account_block_enabledonLets account blocks happen at all. When off, an account block is refused like an administrator block (the source address is blocked instead when it has failed enough).
Counting window (minutes)block_window_minutes60How far back failures count. Saved 1 to 10,080 (the form accepts 5 to 10,080). Also the window for default-username counting and XML-RPC escalation, and it sets a floor for the attempt retention.
Risk score for "suspicious"risk_suspicious_score25A total at or above this is "suspicious". Saved 1 to 100.
Risk score for "block"risk_block_score80A total at or above this blocks the address. Saved 1 to 100. Bot signals are capped at 35 and cannot reach it unless you set this to 35 or lower.

Administrator protection

Setting (label)KeyDefaultWhat it does
Administrator protection ("Apply the stricter limits and dedicated logging to administrator accounts")admin_protection_enabledonApplies the stricter administrator limits. (The dedicated administrator log events are switched by the two log settings below.)
Failed attempts before an admin account is treated as targetedadmin_max_failed5The lower limit for administrator accounts. Saved 1 to 1,000 (the form accepts 1 to 100). Settings shows "Administrator accounts use the stricter limit of 5 failed attempts on the account instead of 20." (the general figure is the higher of the username and email limits). It applies to the username, email, distinct-address and "suspicious" limits; the per-IP block limit is not lowered (see Administrator protection).
Automatically block administrator accountsadmin_account_blockoffWhen on, an administrator account that crosses its limit is blocked and nobody, including you, can sign in as it until you unblock it. Leave it off unless you have a second administrator.
Log successful admin loginslog_admin_successonWrites "Administrator login" events.
Log failed admin loginslog_admin_failureonWrites "Administrator login failure" events.
Trust the address administrators sign in fromtrust_admin_ipsoffAllowlists the address of a completed administrator sign-in for a limited time. See Trusted administrator addresses.
Trusted for (days)trust_admin_ip_days30How long an automatic entry lasts; each sign-in renews it. Saved 1 to 365.
Addresses kept per administratortrust_admin_ip_limit3Automatic entries kept per administrator; the oldest is removed first. Saved 1 to 20.

Bot detection, WooCommerce, default usernames, exploit probing

Setting (label)KeyDefaultWhat it does
Bot detection ("Look for automation signals ...")bot_detection_enabledonScores automation signals (capped at 35). See Bot detection.
Requests per minute that count as high frequencybot_frequency_per_minute10The rate that counts as high; four times it is extreme; six times it per hour is sustained. Saved 1 to 1,000 (the form accepts 1 to 600). Counts requests to the sign-in endpoints only.
Protect the shop sign-in form ("Treat the WooCommerce sign-in form as an authentication endpoint")woocommerce_protectiononRecognises the WooCommerce sign-in form as a sign-in page (only when WooCommerce is active). See WooCommerce sign-in forms.
Default usernames ("Watch for login attempts with default usernames that do not exist here")generic_username_enabledonScores and eventually blocks default usernames that are not accounts.
Block the address after this many attemptsgeneric_username_threshold3Attempts per address inside the counting window. Saved 1 to 100.
Exploit probing ("Block addresses that keep requesting exploit paths")probe_protection_enabledonCounts 404 requests for exploit paths. See Exploit probing.
Block after this many probesprobe_threshold5Probes per address inside the probing window. Saved 2 to 1,000.
Counting window (minutes) (in the exploit-probing card)probe_window_minutes10The probing window. Saved 1 to 1,440.

Sign-in form check, distributed attacks, account discovery, crawlers (1.4.0)

Setting (label)KeyDefaultWhat it does
Form check ("Refuse sign-in posts that did not come from a form this site served")form_origin_enabledonA sign-in post without a valid form token is refused before the password is looked at. See Sign-in form check.
A form stays valid for (hours)form_origin_hours24How long a rendered form's token is accepted. Saved 1 to 168.
Distributed attacks ("Tighten the limits while many addresses attack at once")spray_enabledonThe site-wide state that halves the limits. See Distributed attacks.
Different addresses that start itspray_threshold8Different addresses with a hostile sign-in row inside the window. Saved 3 to 500.
Counting window (minutes) (in the Distributed attacks card)spray_window_minutes60Saved 5 to 1,440.
Keep the tighter limits for (minutes)spray_hold_minutes60Counted from the last time the count was still over the threshold. Saved 5 to 1,440.
REST users list ("Close /wp-json/wp/v2/users to visitors who are not signed in")enumeration_rest_usersonSee Account discovery.
Author numbers ("Answer a visitor's ?author=N with the 404 page ...")enumeration_author_queryonAlso keeps author names out of oEmbed answers. Does nothing with plain permalinks.
Crawler verification ("Verify Googlebot, Bingbot, Applebot, DuckDuckBot, YandexBot and Baiduspider by DNS")crawler_verificationonSee Search-engine crawlers.
The four Settings cards added in 1.4.0: Sign-in form check, Distributed attacks, Account discovery and Search-engine crawlers
The four cards added in 1.4.0, between Exploit probing and XML-RPC.

Sign-up protection (1.7.0)

One card, between Sign-in form check and Distributed attacks. Under its description two pills say whether the My Account registration and the WordPress registration are switched on on your site. See Sign-up protection for what each check does.

Setting (label)KeyDefaultWhat it does
Sign-up protection ("Check sign-ups on the My Account registration form and on wp-login.php?action=register")registration_protectiononMaster switch for every check below, the sign-up record and the XML-RPC watch.
JavaScript checkregistration_js_checkonRefuses a sign-up whose form was sent without the page's JavaScript. Off: the token is printed in the field, the fill time is still measured, and a post without a token is not refused.
Minimum time to fill in the form (seconds)registration_min_seconds4Saved 0 to 60; 0 switches it off.
Hidden fieldregistration_honeypotonAdds a field people never see and refuses a sign-up that fills it in.
Email domain: "Refuse an email domain that cannot receive mail"registration_email_dnsonA null MX, or no mail server and no address at all.
Email domain: "Refuse throw-away domains that receive mail for any made-up name (a wildcard MX)"registration_wildcard_mxonThe wildcard check.
Outdated browserregistration_outdated_browseronRefuses no browser name, an automation tool's name, or a Chrome-based browser or Firefox this many versions behind.
versions behindregistration_browser_max_lag24Saved 6 to 120. 24 is about two years. The card prints today's estimated versions.
Sign-ups per address (accounts per hour)registration_ip_limit3Saved 0 to 100; 0 means no limit.
New accounts that probe XML-RPC ("Block an account that requests xmlrpc.php soon after it was created")registration_xmlrpc_watchonSee Sign-up protection.
minutes after the sign-upregistration_watch_minutes60Saved 5 to 1,440.
Blocked mail serversregistration_blocked_mail_serversemptyOne IP address or range per line, stored like the allowlist (10.0.0.* becomes 10.0.0.0/24); other lines are dropped. Mark as fake adds to it.
Blocked email domainsregistration_blocked_domainsemptyOne domain per line; every address at that domain or any name under it is refused. A pasted email address or URL keeps its domain, international names are stored in their xn-- form, name.* covers every top-level domain of a name, and a bare top-level domain or public suffix (com, co.uk) is dropped. Mark as fake adds to it.
Log sign-ups ("Record every accepted sign-up in the security log")log_registrationsonThe "Sign-up" log lines. Refused sign-ups are logged with the blocked requests.
Settings, Sign-up protection: the master switch, the JavaScript check, the minimum fill time, the hidden field, the two email-domain switches, the outdated-browser limit, sign-ups per address, the XML-RPC watch, the two block lists with one learned entry each, and Log sign-ups
Settings → Sign-up protection with the defaults. The two lists hold what Mark as fake learned from one demo account: its domain's mail server and the domain.

XML-RPC

Setting (label)KeyDefaultWhat it does
Block XML-RPC ("Refuse all XML-RPC requests with a generic 403")xmlrpc_blockonRefuses every XML-RPC request. Turn it off only if a plugin or app genuinely needs XML-RPC.
Allow XML-RPC authenticationxmlrpc_allow_authoffAllows username and password logins over XML-RPC when XML-RPC itself is allowed (Jetpack-style clients, the mobile app). Has no effect while XML-RPC is blocked.
XML-RPC attempt limit per IPxmlrpc_max_failed5Refused XML-RPC requests from one address that cause an address block, and the stricter per-IP limit for XML-RPC sign-ins. Saved 1 to 1,000.
Log XML-RPC activity ("Record XML-RPC requests by method name")xmlrpc_logonLogs allowed XML-RPC calls by method name only.

Blocking behaviour

Setting (label)KeyDefaultWhat it does
Manual blocking ("Show block and unblock controls on the admin screens")manual_blocking_enabledonWhen off, creating a block by hand (the two forms and wp wpus block) is refused. Unblocking is unaffected.
Allow permanent blocks: IP blockspermanent_ip_blockingonLets an IP block be permanent (the manual form checkbox, "Make permanent", --permanent).
Allow permanent blocks: Account blockspermanent_user_blockingonLets an account block be made permanent.
Release automatic blocks after (hours)block_auto_release_hours00 means never. Otherwise automatic, non-permanent blocks lift after this many hours. Saved 0 to 8,760. A wide range that the rotating-address rule blocked has its own number of hours (network_wide_release_hours, Login protection) and lifts even when this is 0.
Longer every time (times)block_escalation_factor1Each repeat automatic block lasts this many times longer than the last, up to 8,760 hours. 1 means the same length. Saved 1 to 10.
Repeat offenders ("Halve the per-IP limits ...")repeat_offender_enabledonHalves the per-IP limits of an address that was blocked automatically and released by the clock.
Remember it for (days)repeat_offender_days30How long the memory lasts, counted from when the earlier block ended. Saved 1 to 365. Also the window for "Longer every time".
Blocked addresses ("Refuse them with a plain 403 before the login page loads")early_block_enabledoffEarly refusal of blocked addresses. See Blocks.

Country rule

Setting (label)KeyDefaultWhat it does
Rulecountry_modeoffoff (Off), deny (Block sign-ins from the countries below) or allow (Only allow sign-ins from the countries below). Any other value is saved as off. A rule with no countries is saved as off.
Where the country comes fromcountry_headerCF-IPCountryOne of CF-IPCountry, CloudFront-Viewer-Country, X-Vercel-IP-Country, X-AppEngine-Country, GeoIP-Country-Code, GEOIP_COUNTRY_CODE. Anything else is saved as CF-IPCountry.
Countriescountry_codesemptyComma-separated, upper-case, sorted ISO 3166-1 alpha-2 codes. Unknown codes are dropped on save.

Logging

Setting (label)KeyDefaultWhat it does
Logging ("Record security events")logging_enabledonMaster switch for the security log.
Events to record: Successful loginslog_successful_loginson"Successful login" events.
Events to record: Failed loginslog_failed_loginson"Failed login" events.
Events to record: Requests refused by a blocklog_blocked_requestson"Blocked login", block, "Suspicious authentication activity" and "Block record deleted" events.
Events to record: Bot detectionslog_bot_detectionon"Bot / automation detected" events.
Events to record: XML-RPC requestslog_xmlrpcon"XML-RPC request" and "XML-RPC blocked" events.
Names typed in failed logins ("Show the username or email that was typed, even when it is not an account on this site")log_attempted_namesonKeeps the typed name in the log row when it is not an account on your site (lower case, 100 characters at most, plain text, searchable, in the CSV as attempted_name). It never feeds a counter. Names already stored stay until log retention removes them. Forced on in emergency mode. See Privacy.
Keep log rows forlog_retention_days90One of 30 days, 60 days, 90 days, 180 days, 1 year (365) or Forever (0). Any other value is saved as 90.
Keep attempt rows for (days)attempt_retention_days30How long the counter rows are kept. Saved 1 to 3,650, never below the counting window rounded up to whole days.

Emails (Notifications)

Since 1.5.0 these live on their own screen, User Security → Emails (see Emails); the Notifications card on Settings only points there. They are the same settings in the same option, saved by their own form: the Settings form never touches them and the Emails form never touches the rest. Labels below are those of the Emails screen.

Setting (label)KeyDefaultWhat it does
Send me email about security eventsnotify_enabledonMaster switch for alerts and the digest. The sign-in code and the test email do not depend on it.
Send them tonotify_emailemptyOne valid email address. Empty, or invalid, means the site administration address (Settings → General).
Send a digestdigest_frequencyweeklyoff (Never), daily (Every day), weekly (Every week) or monthly (Every month). Anything else is saved as weekly.
Accounts to watchnotify_rolesadministrator,editor,author,shop_managerRole slugs behind the "watched account" alerts. Unknown roles are dropped and administrator is always kept.
Tell me when: An address or network was blockednotify_ip_blockedonSee Alerts.
Tell me when: An account is attacked from several addressesnotify_account_targetedonAccount blocks and distributed attacks.
Tell me when: Somebody tries the password of a watched accountnotify_admin_failed_loginsonAlso controls the "Administrator block refused" alert.
Tell me when: Right password, wrong sign-in codenotify_two_factor_failedonNeeds the email code to be on.
Tell me when: A watched account signs innotify_privileged_signinoffOne email per account and address per repeat window.
Tell me when: A burst of traffic on the sign-in pagenotify_attack_spikeonSee Bot detection.
Tell me when: XML-RPC is being hammerednotify_xmlrpc_attackonRefused XML-RPC requests.
Tell me when: A distributed sign-in attack startsnotify_sprayonOne email per episode of the distributed-attack state (1.4.0).
Tell me when: A sign-up was refusednotify_registration_refusedoffOne email per repeat window for the whole site (1.7.0). See Alerts.
At most one email per alert every (minutes)notify_throttle_minutes15At most one email per alert type and subject in this window. Saved 1 to 1,440. Does not apply to the digest.

Reverse proxies

Setting (label)KeyDefaultWhat it does
Trust proxy headers ("Read the client address from a proxy header instead of REMOTE_ADDR")trust_proxy_headersoffUse the address in the chosen header. Only turn it on when infrastructure you control sets the header.
Proxy headerproxy_headerX-Forwarded-ForOne of X-Forwarded-For, CF-Connecting-IP, X-Real-IP, True-Client-IP. Anything else is saved as X-Forwarded-For.

Two-factor authentication (email codes)

Setting (label)KeyDefaultWhat it does
Ask for a sign-in codetwo_factor_enabledoffMaster switch. Off by default because it can refuse a correct password when mail fails.
Who is askedtwo_factor_rolesadministrator,editor,author,shop_managerRole slugs. Administrators, and any account that can manage_options, are always asked.
Every sign-in form ("Withhold the login cookie from a sign-in that skipped the code ...")two_factor_all_formsonThe safety net for forms that never call authenticate. Untick only if a social-login or magic-link plugin conflicts.
Code lifetime (minutes)two_factor_code_minutes10How long a code stays usable. Saved 1 to 60.
Wrong codes per emailtwo_factor_max_attempts5Wrong codes before the code is burnt and a new one is sent. Saved 1 to 10.
Remember a browser for (days)two_factor_device_days30How long a browser that gave a correct code is not asked again. Saved 1 to 365.

Privacy and emergency mode

Setting (label)KeyDefaultWhat it does
Sign-in notice ("Show a short privacy notice under the sign-in forms")login_notice_enabledoffPrints the notice on the sign-in forms. See Privacy tools.
Notice textlogin_notice_textemptyYour wording; empty uses the built-in wording. Up to 600 characters; links, bold, italics and line breaks only. {privacy_policy} becomes a link to your privacy policy page.
Delete plugin data on uninstall ("Remove every table and option when the plugin is deleted")delete_data_on_uninstallonWhen ticked, deleting the plugin removes its tables, options, transients, user meta and capability. Untick it first to keep your block list and logs. Deactivating never deletes anything.
Emergency mode (no field on this screen; use Tools)emergency_modeoffSwitched only on User Security → Tools. A save from the Settings form never changes it.
Other plugins' keys.Because the option is one array, a plugin that adds its own keys through the wpus_settings_sanitized filter keeps them: saving the Settings screen keeps keys this plugin does not know.

8. Developer reference

The plugin registers no shortcodes, blocks, REST routes, AJAX handlers or widgets (apart from the WordPress Dashboard widget), and it has no template files to override. What it exposes is WP-CLI commands, actions and filters, one admin POST endpoint and one export endpoint, a capability, one constant you can define, two scheduled events, five tables, one settings option and a few transients. The prefix is wpus_ (classes WPUS_).

WP-CLI

All commands start with wp wpus and are available when WP-CLI is loaded. They write through the same code as the admin screens, so the log and the hooks behave the same way. They are the documented way back in when you are locked out of the browser.

CommandWhat it does
wp wpus unblock [--account=<account>] [--ip=<ip>] [--reason=<reason>] [--all]Unblocks an account, an address, or every active block. Pass at least one of --account, --ip or --all ("Nothing to do: pass --account=<username>, --ip=<address> or --all."). --all wins over the other two, and --account wins over --ip (only one is acted on). --account takes a username or email address and clears every row of the account. --ip takes an address, or a blocked network or range such as 203.0.113.0/24 or 203.0.112.0/20 (an IPv6 network is a /64, /48 or /32). --all pages through all active blocks of every type and prints each. --reason is recorded in the log (default "Unblocked from WP-CLI").
wp wpus blocks [--type=<type>] [--format=<format>]Lists active blocks (up to 500). --type is ip, account or user. --format is table (default), csv, json, yaml, count or ids. Columns: id, type, value, username, status, permanent, attempts, reason, blocked_at. With nothing blocked it prints "Nothing is blocked."
wp wpus block [--ip=<ip>] [--account=<account>] [--reason=<reason>] [--permanent]Blocks an address, a network or a range (--ip=198.51.100.7, --ip=45.133.7.0/24 or --ip=178.239.192.0/20; any address with a size is turned into the network it belongs to; the sizes are an IPv4 /24, /20, /16 and an IPv6 /64, /48, /32) or an account (--account wins when both are given). --permanent applies to addresses and needs Allow permanent blocks (IP blocks) to be on. Default reason "Blocked from WP-CLI". It is a manual block: no expiry, and it can replace an active automatic block. Unlike the screen, the command does not refuse your own address. It refuses an allowlisted or invalid address, a network with a trusted address inside and any other size, and obeys Manual blocking.
wp wpus stats [--format=<format>]Prints the Dashboard counters for the default period (24 hours) as metric and value: blocked_ips, blocked_users, failed_logins, successful_logins, blocked_attempts, bots_detected, xmlrpc, xmlrpc_blocked, admin_failures, admin_logins, suspicious, new_ip_blocks, new_user_blocks, allowlist. Formats: table, csv, json (one flat object) and yaml. (The command's help also mentions count, which falls back to table.)
wp wpus logs [--limit=<n>] [--event=<event>] [--format=<format>]Shows the newest security events. --limit is 1 to 500 (default 20). --event is an event type such as admin_login_failed. Formats as for blocks. Columns: id, created_at, event, result, risk, ip, username, reason. CSV output defuses spreadsheet formulas.
wp wpus allow <entry> [--label=<label>]Adds an address, CIDR range or 10.0.0.* shorthand to the allowlist.
wp wpus unallow <entry>Removes an entry. Give it exactly as stored (10.0.0.0/24, not 10.0.0.*); wp wpus allowlist shows the stored form. Errors with "That address is not in the allowlist." when there is no match.
wp wpus allowlist [--format=<format>]Lists the allowlist. Columns: id, entry, type, label, added. Formats as for blocks.
wp wpus retentionRuns the retention cleanup now and reports the rows removed. Blocks are not touched.
wp wpus releaseReleases automatic blocks whose time has run out (the same job that runs hourly). With emergency mode on it warns and releases nothing. With automatic release off it says only blocks that already carry an expiry are looked at.
wp wpus country [--off]Without a flag, prints the rule, countries and header. --off switches the country rule off and keeps your list and header choice.
wp wpus spray [--off]Without a flag, says whether a distributed sign-in attack is on (since when, how many addresses, until when) or how many different addresses attacked inside the window and the threshold. --off ends the state now; the rows that started it cannot start it again, the next spray can. 1.4.0.
wp wpus signups [--days=<days>] [--format=<format>]Recent sign-ups on the two registration forms, accepted and refused, with the reason (1.7.0). --days defaults to 7. Formats table (default), csv, json, yaml, count.
wp wpus mark-fake <user> [--yes]Marks an account as fake (user ID, login or email): blocks its sign-up address, adds its email domain's own mail servers and the domain to the sign-up block lists, and deletes it, with the same rules as the Users screen. Asks first unless --yes (1.7.0).
wp wpus two-factor [--user=<user>] [--devices] [--disable] [--revoke] [--format=<format>]Without a flag, says whether the email code is on, for which roles, and how many browsers are remembered. --devices lists remembered browsers (columns id, user, browser, ip, last, expires). --revoke forgets remembered browsers without switching the feature off. --disable switches the feature off for the whole site and forgets every remembered browser, keeping every other setting. --user (a username, an email address or a user ID) narrows --devices and --revoke to one account; --disable ignores it and always acts on the whole site.
wp wpus digest [--days=<days>] [--send]Prints the security digest. --days is 1 to 35 (default: one period of the configured schedule, or 7 when the digest is off). --send emails it to the notification address instead; it does not change the schedule.
wp wpus diagnose [--format=<format>]Lists every callback on the sign-in filters (hook, priority, callback, from, after_ours) and warns about other login-limiting plugins and callbacks after priority 100. Formats table, csv, json, yaml. The address check needs a web request, so it lives on Tools.
wp wpus export <what> [--days=<days>]Prints an export to the screen: logs, blocks, allowlist (CSV) or settings (JSON). --days limits the log to the last N days. The export is recorded in the log.
wp wpus import-settings <file>Applies a settings file made by wp wpus export settings, with the same rules as the screen (the thirteen settings are never imported).
# Back in after a lockout
wp wpus blocks
wp wpus unblock --account=your-admin-login --reason="Locked out"
wp wpus unblock --ip=203.0.113.10
wp wpus two-factor --disable
wp wpus country --off

# Move a configuration from staging
wp wpus export settings > settings.json
wp wpus import-settings settings.json

# Reports
wp wpus logs --event=admin_login_failed --limit=50 --format=csv
wp wpus export logs --days=30 > security-log.csv
wp wpus digest --send --days=7

Actions

ActionParametersFires when
wpus_ip_blocked$ip, $block_id, $reason, $manual (bool)A block row for an address, a network or a range is written ($ip is then the network in CIDR form, such as 45.133.7.0/24), new or replacing an old one. Not fired when an automatic rule only refreshes an active block.
wpus_ip_block_created$row (array), $manual (bool), $reasonA new address, network or range block starts (or a lifted one starts again). The alert listens to this.
wpus_account_block_created$row, $account (resolved account array), $reasonA new account or user-ID block row starts. Fires once per row, up to three times per account.
wpus_account_block_refused$result (risk result), $account, $reasonA block of an account was asked for but refused (administrator protection, or account blocking is off).
wpus_block_unblocked$row (as it was before), $reasonA block row is lifted by a person, WP-CLI or the automatic release. Fires once per row.
wpus_login_blocked$action (type, value, reason), $accountA sign-in was refused because the address or account is blocked, XML-RPC authentication is off, or a country refused it (country refusals only when logged, at most every 10 minutes).
wpus_login_success$user (WP_User), $is_admin (bool)A successful sign-in was processed. Only fires while Logging is on.
wpus_admin_login_failed$result, $accountA wrong password for an administrator account. It no longer sends email itself.
wpus_privileged_login_failed$result, $accountA wrong password for an account in a watched role, administrators included. Drives the alert.
wpus_suspicious_activity$result, $accountA suspicious decision was logged (at most one per address and identifier every 10 minutes).
wpus_bot_detected$detection (signals, score, reason, is_bot), $accountA failed attempt carried bot signals.
wpus_attack_spike$frequency (per_minute, per_hour), $ipAn address reached the high-frequency rate on a sign-in endpoint (at most once per address every 5 minutes).
wpus_xmlrpc_blocked$ipA request was refused while XML-RPC is blocked.
wpus_network_left_alone$network, $count, $whyA network or range crossed its threshold but was left alone on purpose. $network is the canonical network or range, $count the addresses counted, $why one of allowlist (a trusted address or an Allowlist entry is inside), signed_in (somebody signed in from it in the last 90 days) or no_signin_log (the successful sign-ins of customers are not logged). At most once an hour per network.
wpus_allowlist_added$entry, $idAn allowlist entry was added (by hand, WP-CLI or the trusted-address feature).
wpus_allowlist_removed$entry, $idAn allowlist entry was removed.
wpus_two_factor_failed$user (WP_User), $whyA sign-in code could not be delivered, was wrong, ran out, or a sign-in was voided. $why is invalid, exhausted, expired, mail_failed, wait or voided.
wpus_form_origin_refused$refusal (array: form = wp-login or woocommerce, status = missing, malformed, invalid or expired), $accountA sign-in post was refused because it carried no valid form token (1.4.0).
wpus_spray_detected$state (array: started, until, addresses, extended), $countA distributed sign-in attack started: this many different addresses attacked the sign-in forms inside the window. Once per episode (1.4.0). The alert listens to this.
wpus_registration_refused$code, $context (array: form, ip, email, domain and the check's details)Sign-up protection refused a sign-up (1.7.0). $code is the reason code, such as honeypot or wildcard_mx.
wpus_registration_recorded$user_id, $contextAn account created through one of the two registration forms got its sign-up record (1.7.0).
wpus_user_marked_fake$user_id, $learned (array: ip, domain, servers, source)An account was marked as fake; it fires after the account was deleted (1.7.0).
wpus_author_query_refusednoneA visitor's ?author=N was answered with the 404 page (1.4.0).
<?php
// Write every new address block to the PHP error log.
add_action( 'wpus_ip_block_created', function ( $row, $manual, $reason ) {
	if ( is_array( $row ) ) {
		error_log( sprintf( 'Blocked %s (%s): %s', $row['value'], $manual ? 'manual' : 'automatic', $reason ) );
	}
}, 10, 3 );

Filters

FilterParametersWhat it changes
wpus_settings$settingsThe effective settings for the request, after emergency mode. Force values from code. The Settings screen still shows the saved values.
wpus_settings_sanitized$clean, $inputThe settings array right before it is stored.
wpus_risk_signals$result, $context, $accountThe risk result after the built-in signals and before the decision. Append to signals, score and reasons, or set a block_action. Runs only for requests that get scored (not allowlisted, not already blocked, protection on).
wpus_bot_signals$signals, $contextThe bot signals before they are scored and capped. Each signal is an array with code, score and reason.
wpus_log_data$row, $eventA log row right before it is written.
wpus_manage_capability$capThe capability that gates every screen and action (default wpus_manage_security). The WPUS_MANAGE_CAP constant takes precedence.
wpus_generic_login_error$messageThe one message every credential failure returns.
wpus_erase_attempts$allowed (default true)Whether a privacy erasure also clears attempt rows.
wpus_attributable_ip$ok, $ipWhether the automatic rules that block or trust a single client may act on this address.
wpus_trust_admin_ip$ok, $ip, $userWhether an address may be allowlisted after an administrator signed in.
wpus_generic_usernames$namesThe default-username list (lower-case).
wpus_probe_rules$rulesThe exploit-probe patterns (label to regular expression), matched against the lower-cased, URL-decoded path without the query string.
wpus_two_factor_active$active (default false)Return true when another two-factor system already protects sign-ins; the email code then stands down.
wpus_two_factor_applies$yes, $userWhether one account is asked for a sign-in code.
wpus_two_factor_resend_seconds$seconds (default 60)The minimum seconds between two code emails to one account.
wpus_digest_level$level, $dataThe threat level (low, medium, high; anything else is ignored).
wpus_email_message$message, $contextAny plugin email before it is laid out (since 1.3.0): tone (danger, warning, info, success), label, title, intro, code, facts (label and value pairs), stats, sections, callout, notes, button. Every string is plain text (it is escaped). $context says which email: ip_blocked, account_blocked, failed_signin, two_factor_alert, signin, block_refused, distributed_attack, attack_spike, xmlrpc, digest, code, test.
wpus_email_html$html (default true), $contextReturn false to send plain text only (the same words as the text part of the designed email).
wpus_digest_lines$lines, $dataDeprecated since 1.3.0 (use wpus_email_message with the context digest). Still honoured: when a callback changes the lines, the digest shows those lines inside the card.
wpus_export_row_cap$cap (default 100000)The most rows one exported CSV holds.
wpus_form_origin_required$required (bool), $formReturn false to exempt a sign-in post from the form check: for a hand-made form that posts to wp-login.php without the login_form hook (1.4.0).
wpus_country_for_ip$code (two upper-case letters, or an empty string), $ip (an address, or a network such as 45.133.7.0/24)Replaces the country the Dashboard cards show for an address (1.6.0). Return a two-letter ISO code, or an empty string for "unknown"; anything else is treated as unknown. It never affects blocking or the country rule.
wpus_form_origin_lifetime$secondsHow long a rendered sign-in form stays valid (the setting in seconds; floor 60).
wpus_form_origin_message$messageThe "reload and try again" message of a refused form post.
wpus_known_crawlers$crawlersThe crawlers that can be verified: key to markers (User-Agent fragments), domains (reverse-name suffixes) and label.
wpus_crawler_reverse_lookup, wpus_crawler_forward_lookup$result, $ip / $host, $v6Return a string (reverse) or an array (forward) to replace the DNS lookups, for a resolver of your own or for tests.
wpus_shared_edge_ranges$ranges (CIDR list)CDN edge ranges that are never treated as one client (Cloudflare's published ranges by default).
wpus_registration_required$required (bool), $form (wc-register or wp-register)Return false to exempt a registration form from Sign-up protection (1.7.0).
wpus_registration_refused_message$message, $codeThe message a refused sign-up shows.
wpus_dns_lookup$answer (null), $host, $type (MX, A, AAAA)Return an answer to replace one DNS lookup of the email-domain checks: a list (MX: target and pri; A and AAAA: addresses), or false for a resolver error.
wpus_browser_current_major$major, $family (chromium or firefox), $nowCorrects the estimate of today's Chrome or Firefox version.
wpus_never_learn_domains$domainsEmail domains Mark as fake never adds to the block list: the free mail providers by default (name.* means every top-level domain of a name). Your own domains are always added.
wpus_wildcard_mx_exempt$domainsMail providers whose wildcard MX is a feature (sub-domain addressing, privacy aliases), never treated as throw-away.
<?php
// Treat a private lab network as one identifiable client each (local development only).
add_filter( 'wpus_attributable_ip', '__return_true' );

// Add your own default usernames.
add_filter( 'wpus_generic_usernames', function ( $names ) {
	$names[] = 'webadmin';
	return $names;
} );

// Stop treating a legitimate path as an exploit probe.
add_filter( 'wpus_probe_rules', function ( $rules ) {
	unset( $rules['php info page'] );
	return $rules;
} );

// Exempt one single-sign-on account from the email code.
add_filter( 'wpus_two_factor_applies', function ( $yes, $user ) {
	return 'sso-service' === $user->user_login ? false : $yes;
}, 10, 2 );

// Add a scoring rule: 10 points for any request on the XML-RPC endpoint.
add_filter( 'wpus_risk_signals', function ( $result, $context, $account ) {
	if ( 'xmlrpc' === $result['endpoint'] ) {
		$result['signals'][] = array( 'code' => 'my_rule', 'score' => 10, 'reason' => 'My rule' );
		$result['score']    += 10;
		$result['reasons'][] = 'My rule';
	}
	return $result;
}, 10, 3 );

The risk result passed to wpus_risk_signals is an array with the keys decision (allow, suspicious or block), level, score, reasons, signals, block_action (null or an array with type (ip or account), value and reason), allowlisted, admin_target, observed (ip_failures, ip_username_count, ip_account_count, account_failures, account_unique_ips), counters, thresholds, identifier, account, endpoint (wp-login, xmlrpc, rest, wp-admin, woocommerce or other), ip and bot (the bot detection result). The filter runs before the decision, so score and block_action changes take effect.

WordPress and WooCommerce hooks the plugin uses

Where the plugin replaces a result, a plugin hooked earlier on the same hook is ignored for that result, and one hooked later can still change it. The priorities below are the ones the plugin registers.

HookPriorityWhat the plugin does
authenticate (filter)1Returns a generic error when the address or account is blocked, the country is refused or XML-RPC authentication is off, before WordPress checks the password.
authenticate99The email sign-in code check: returns an error until a valid code is entered.
authenticate100The final word: replaces the result with the generic error for a block decided at priority 1, records a failure, runs the risk engine and rewrites credential errors to the generic message.
wp_login (action)5, 10, 20, 305: removes a stray remembered-browser cookie for an account the code does not cover. 10: logs the sign-in. 20: trusts the administrator's address (when enabled). 30: sends the "watched account signs in" alert (when enabled).
login_init (action)0 and 100: early refusal of a blocked address (when enabled). 10: counts the request on the login page.
init (action)1 and 101: refuses blocked XML-RPC requests, and refuses blocked addresses on XML-RPC when early refusal is on. 10: registers the licence client (admin, cron and WP-CLI only).
login_errors (filter)100Replaces credential error messages with the generic message.
rest_authentication_errors (filter)100Replaces a credential error with one generic 401.
application_password_is_api_request (filter)10Returns false for a blocked address or account so WordPress skips application-password authentication.
application_password_failed_authentication, application_password_did_authenticate (actions)10Records a failed application-password attempt; logs a successful one.
xmlrpc_enabled (filter), xmlrpc_call (action), wp_die_xmlrpc_handler (filter)10Switches XML-RPC off while blocked; logs allowed calls by method name; replaces core's die handler while blocked so the refusal is a real 403.
send_auth_cookies (filter)10Withholds the login cookies from a covered account's sign-in that skipped the code check (when Every sign-in form is on).
login_form (action), login_form_middle (filter), login_message (filter)10Print the sign-in code box, add it to an embedded wp_login_form(), and show the "needs a sign-in code" message. login_form also carries the sign-in notice.
woocommerce_login_form, woocommerce_login_form_end (actions)10Print the code box on the WooCommerce form; print the sign-in notice at the end of the form.
wp_loaded (action)5 and 105: observes a WooCommerce sign-in submission. 10: runs the schema check and re-schedules the hourly release event.
template_redirect (action)0Looks at a 404 for an exploit path.
user_register, profile_update, after_password_reset, deleted_user, set_user_role (actions)10Remember a new account for the sign-in code safety net; forget remembered browsers after a password change or reset; remove a deleted or demoted administrator's automatic allowlist entries.
woocommerce_register_form, register_form (actions)10Print the sign-up token field and the hidden field and load assets/js/signup.js in the footer (1.7.0).
woocommerce_process_registration_errors (filter)5Sign-up protection on the My Account form (only that form fires it; at 5 so it answers before a CAPTCHA plugin at 10).
registration_errors (filter)5Sign-up protection on wp-login.php?action=register.
user_register (action)20Writes the sign-up record of an account one of the two forms created.
user_row_actions, bulk_actions-users, handle_bulk_actions-users (filters), admin_notices (action)10Mark as fake on the Users screen. The bulk action only opens the confirmation panel: WordPress removes the nonce before a custom bulk action runs.
plugins_loaded, wp_initialize_site, cron_schedules5, 20, 10Boot the plugin; create tables for a new site on a network; add a daily schedule on multisite when missing.
Admin: admin_menu, admin_init, admin_post_wpus_do, admin_post_wpus_export, wp_dashboard_setup, admin_notices, admin_enqueue_scripts10Menus, settings registration and schema check, the two endpoints, the Dashboard widget, notices, and the screen assets.
sanitize_option_wpus_settings (filter), update_option_wpus_settings, add_option_wpus_settings (actions)10The Settings API check and save callback (registered by register_setting()); it validates every key, refuses the save for a signed-in user who may not manage the plugin, and logs the change. The two actions clear the plugin's per-request settings cache after a write.
wp_privacy_personal_data_exporters, wp_privacy_personal_data_erasers (filters)10Register the "Security log" exporter and eraser.

The bundled licence client also hooks plugin_action_links_<plugin file>, in_plugin_update_message-<plugin file>, pre_set_site_transient_update_plugins, plugins_api (priority 20), http_request_args, http_request_host_is_external and upgrader_process_complete, only where it is loaded.

Admin endpoints and request parameters

  • admin-post.php?action=wpus_do is the single endpoint for every change made on the plugin screens. It requires the managing capability and a valid nonce (nonce action wpus_action, field _wpnonce), runs the action named by wpus_action, writes a "Security settings changed" log row ("Manual action "<name>" from the admin screens: completed" or "refused"), stores a notice for the user for five minutes and redirects back. The actions are: block_ip, block_user, unblock, bulk_unblock, permanent, bulk_permanent, delete_block, bulk_delete_block, bulk_delete_logs, add_allowlist, remove_allowlist, emergency, purge_logs, purge_attempts, run_retention, revoke_device, revoke_all_devices, test_mail, send_digest and import_settings. An unknown name answers "That action is not recognised."
  • admin-post.php?action=wpus_export&what=<logs|blocks|allowlist|settings>[&days=N]&_wpnonce=... streams a download (see Export and import). days limits the log only.
  • Settings are saved through options.php with the option group wpus_settings_group and the option wpus_settings. A filter on option_page_capability_wpus_settings_group makes WordPress require the plugin's capability instead of manage_options. A signed-in user who may not manage the plugin changes nothing.
  • Screen addresses: admin.php?page=wpus-dashboard, wpus-logs, wpus-blocked-ips, wpus-blocked-users, wpus-allowlist, wpus-settings, wpus-tools.
  • Query and form fields: Dashboard wpus_period (24h, 7d, 30d, 365d, all). Logs wpus_date_from, wpus_date_to (YYYY-MM-DD), wpus_ip, wpus_username, wpus_email, wpus_event, wpus_risk, wpus_result, wpus_admin, s. Block lists wpus_status (active, blocked, permanent, unblocked or empty for any), wpus_period, s. Forms wpus_row[] (row IDs), wpus_bulk and wpus_bulk2 (the top and bottom bulk-action selects), wpus_ip, wpus_identifier, wpus_reason, wpus_permanent, wpus_entry, wpus_label, wpus_state (on or off, for emergency mode), wpus_days, wpus_device, wpus_settings_json and wpus_filter (the filter button). The sign-in code is the POST field wpus_code; the redirect after a voided sign-in adds wpus_2fa=required to the login URL.
  • Mark as fake (1.7.0): users.php?wpus_fake_confirm=<ids> shows the confirmation panel (comma-separated user IDs; nothing changes). Its form posts wpus_action=mark_fake and wpus_user[] to admin-post.php?action=wpus_do with the plugin's nonce.

Capability, constant and roles

  • Capability wpus_manage_security gates every screen, button, download and the settings save. It is added to the administrator role on activation (on multisite only when a super admin activates). A role you create later does not get it automatically; grant it with a role editor.
  • Constant WPUS_MANAGE_CAP: define it in wp-config.php to require another capability, for example define( 'WPUS_MANAGE_CAP', 'manage_options' );. It takes precedence over the filter wpus_manage_capability.
  • Constants the plugin defines (each only if not already defined): WPUS_VERSION (1.6.0), WPUS_DB_VERSION (1.2.0, the schema did not change in 1.3), WPUS_FILE, WPUS_DIR, WPUS_URL, WPUS_SLUG (wp-user-security-guard) and WPUS_TEXTDOMAIN.
  • The function wpus_guard() returns the main plugin object.

Scheduled events

EventScheduleWhat runs
wpus_daily_maintenanceDaily (first run about an hour after activation)The security digest check, the log and attempt retention cleanup, removal of expired automatic allowlist entries, and removal of expired remembered browsers.
wpus_release_blocksHourlyReleases automatic blocks whose expiry has passed. It only looks at blocks that carry an expiry: wide ranges blocked by the rotating-address rule always do (48 hours by default), other automatic blocks only when automatic release is on. It does nothing while emergency mode is on.

Both are removed on deactivation. The hourly event is re-created on wp_loaded when it is missing; the daily event is created at activation (and for a new site on a network), so deactivating and activating the plugin re-creates it. On a multisite network the plugin adds a daily schedule if the site has none.

Database tables

Five tables, created with dbDelta(), each with the site's table prefix. All times are site-local (WordPress timezone) DATETIME values. All SQL lives in one class, WPUS_Database, with prepared statements.

{prefix}wpus_attempts (counters)

Failed and refused attempts only. Columns: id BIGINT unsigned auto-increment (primary key), created_at DATETIME, ip_address VARCHAR(45), username VARCHAR(100), email VARCHAR(100), user_id BIGINT unsigned, endpoint VARCHAR(16) (wp-login, xmlrpc, rest, wp-admin, woocommerce or other), result VARCHAR(16) (failed or blocked), risk_score SMALLINT unsigned, risk_level VARCHAR(12), reason VARCHAR(191), user_agent VARCHAR(191) (present, but the plugin never writes a value into it). Indexes on time, address, username, email, user, result and the combinations address and time, username and time, email and time. The rotating-address rule counts these rows by address.

{prefix}wpus_blocks

Columns: id, block_type VARCHAR(16) (ip, account or user), value VARCHAR(191) (the address, the network or range in CIDR form such as 45.133.7.0/24 or 178.239.192.0/20, the normalised username or email, or the user ID), username, email, user_id, status VARCHAR(16) (blocked, permanent or unblocked), permanent TINYINT, reason VARCHAR(191), detail TEXT (JSON: counters, trigger, escalation level), attempt_count BIGINT, unique_ips INT, first_detected, last_detected, blocked_at, blocked_by (administrator's user ID; 0 for automatic blocks and blocks made from WP-CLI), unblocked_at, unblocked_by, unblock_reason VARCHAR(191) (the marker "Released automatically" for an automatic release), expires_at (only automatic, non-permanent blocks: with automatic release on, and wide ranges always unless their release is set to 0) and block_count INT (starts at 1, increased on every re-block). Unique key on block_type and value; indexes on expiry, status, type and status, user and blocked_at.

{prefix}wpus_logs (the security log)

Columns: id, created_at, event_type VARCHAR(32), risk_level VARCHAR(12), risk_score SMALLINT, ip_address VARCHAR(45), user_id, username, email, endpoint VARCHAR(16), request_method VARCHAR(8), user_agent VARCHAR(191), result VARCHAR(16), reason VARCHAR(191), is_admin TINYINT and context TEXT (redacted JSON). Indexes on time, administrator flag, event, risk, address, user, username, email and result, with combinations.

{prefix}wpus_allowlist

Columns: id, entry VARCHAR(191) (canonical address or CIDR range, unique), entry_type VARCHAR(8) (ip or range), label VARCHAR(191), added_by (the user ID of the administrator who added it, or whose sign-in created an automatic entry; 0 from WP-CLI), added_at and expires_at (NULL for entries typed in by hand).

{prefix}wpus_devices (remembered browsers)

Columns: id, user_id, device_hash CHAR(64) (SHA-256 of the cookie token), ua_hash CHAR(64) (SHA-256 of the User-Agent string), label VARCHAR(191), ip_address VARCHAR(45), created_at, last_seen_at and expires_at. Unique on user_id and device_hash.

Options, user meta, transients and cookies

KindNameHolds
Optionwpus_settingsAll settings (autoloaded, one read per request).
Optionwpus_db_versionThe installed schema version (autoloaded).
Optionwpus_digest_lastThe Unix time the last digest was sent, or the time the clock started. Not autoloaded.
Option (licence client)_wp-user-security-guard_licence_key, _wp-user-security-guard_key_statusThe licence key and its status (active or inactive). Not autoloaded. Not removed by this plugin's uninstall.
User metawpus_2fa_pendingA hash of the outstanding sign-in code.
User metawpus_2fa_sentWhen that code was emailed (Unix time).
User metawpus_2fa_triesWrong codes entered for the current code.
User meta_wpus_signupThe sign-up record of an account created through one of the two registration forms: time, address, browser (191 characters), form, whether the address was one client, the email domain's mail servers (1.7.0). Part of the privacy export and erasure; removed with the account and by uninstall.
Transientwpus_allowlist_cacheThe allowlist (10 minutes).
Transientwpus_notice_<user id>The result notice after an action (5 minutes).
Transientwpus_notify_<hash>The alert throttle per type and subject (the repeat window).
Transientswpus_rl_60_<hash>, wpus_rl_3600_<hash>Request counters per address for a minute and an hour.
Transientwpus_gu_<hash>Default-username counter per address (the counting window).
Transientwpus_pr_<hash>Exploit-probe counter per address (the probing window).
Transientswpus_susp_<hash>, wpus_country_log_<hash>, wpus_early_log_<hash>, wpus_rate_log_<hash>, wpus_xmlrpc_log_<hash>Log throttles so one address cannot fill the log (10 minutes, 10 minutes, 10 minutes, 5 minutes, 1 minute).
Transientwpus_net_skip_<hash>Throttle for the log entry "N addresses of <network> are attacking, but the network was not blocked" (one per network per hour).
Transientwpus_xmlrpc_alert_skippedHow many blocked XML-RPC requests were not mailed since the last alert (one email per repeat window for the whole site; kept up to a day).
Transientwpus_spray_stateThe distributed-attack state (started, until, addresses, extended) while it lasts; wpus_spray_checked keeps the count to one query a minute; wpus_spray_cleared remembers when an administrator ended it (the window). 1.4.0.
Transientwpus_crawler_<hash>Whether an address that claims to be a crawler really is one (verified or fake, one day). 1.4.0.
Transientwpus_xrb_<hash>One "XML-RPC blocked" log line per address per ten minutes. 1.4.0.
Transientwpus_mxd_<hash>What the DNS said about an email domain: its mail servers, whether it takes mail for any name (12 hours, 10 minutes after an error). 1.7.0.
Transientwpus_signup_alert_skippedHow many refused sign-ups were not mailed since the last "A sign-up was refused" alert. 1.7.0.
Site transient (licence client)wpxh_licence_check_v2The cached update information (12 hours, or 1 hour after a failed check).
Transient (licence client)wpxh_licence_msg_<user id>The result message shown after activating, deactivating or emailing a key (2 minutes). Not removed by this plugin's uninstall; it expires by itself.
Cookiewpus_device_<user id>The remembered-browser token. HttpOnly, SameSite=Lax, Secure on HTTPS.

The hashes in transient names are MD5 of the address (or of the throttle subject), not secrets.

Licence client constants and filters

The bundled client (WPXH_Licence_Client_V2) talks to https://wpexpertshub.com/wp-json/wphub-licence/v1/. The constant WPXH_LICENCE_SERVER and the filter wpxh_licence_server override the server address, and wpxh_licence_sslverify turns certificate checking off (it is automatically off only for a local development server such as .local, .test or localhost). The first copy of the class loaded by any WpExperts Hub plugin is the one that runs, and one screen manages all of those plugins.

File layout

  • wp-user-security-guard.php: constants, class loading, activation and deactivation hooks, licence registration.
  • uninstall.php: removes data only when Delete plugin data on uninstall is ticked.
  • includes/: one class per concern (database, settings, request, roles, allowlist, logger, country, offline country lookup (geo), dashboard cards (widgets), block lifecycle, IP blocker, user protection, bot detection, generic usernames, probe protection, network protection (rotating addresses and ranges), insights (the summary card), trusted admins, WooCommerce, risk engine, login protection, two-factor, XML-RPC protection, admin protection, notifications, digest, diagnostics, export, system info, privacy, list tables, admin, WP-CLI and the main container).
  • includes/class-wpus-registration.php, class-wpus-email-domain.php, class-wpus-browser-age.php: Sign-up protection, the email-domain checks and the browser estimate (1.7.0).
  • data/: country-ranges.dat (the compact IP to Country Lite file read by the geo class, 1.6.0) and dbip-licence.txt (the attribution).
  • admin/views/: dashboard.php, logs.php, blocks.php, allowlist.php, settings.php, tools.php.
  • assets/css/admin.css and assets/js/admin.js: loaded only on the plugin screens and on the WordPress Dashboard (for the widget), and only for users who can manage the plugin. The script has no dependencies; it adds confirmations, bulk-action checks, the country filter, the role pickers and the settings navigation.
  • assets/js/signup.js: the sign-up JavaScript check (no dependencies), loaded only on a page that shows one of the two registration forms (1.7.0).
  • licence/: the bundled licence and update client.
  • languages/wp-user-security-guard.pot: the translation template. No translations are shipped, and load_plugin_textdomain() is called only when the plugin is not in the folder wp-content/plugins/wp-user-security-guard.

9. Privacy

User Security Guard keeps its security data in your own database and sends none of it to WpExperts Hub or anyone else. The only outside contact is the licence and update check, described below.

TopicDetail
What is stored Attempts table: for each failed or refused request, the time, IP address, the account's username and email (when the account exists, or the email-like text typed), the endpoint, the result, a risk score and level and a short reason. (The table has a user-agent column, but the plugin never fills it; the user agent is kept in the log table only.)
Log table: the same kind of details for each logged event, plus a small redacted JSON context. Successful sign-ins are stored here (username, email, IP address, roles, whether "remember me" was ticked). For a failed sign-in with a name that is not an account on your site, the context also holds the name that was typed (Settings → Logging → Names typed in failed logins, on by default). The rotating-address rule reads the successful sign-ins of this table (the address only) to see whether real people use a network; nothing leaves the site.
Blocks table: the blocked address, network or range, username, email or user ID, the reason, counters and dates, and who unblocked it.
Allowlist table: the address or range, your label, who added it and when.
Devices table (email sign-in code only): hashes, a short browser label, the IP address and dates for remembered browsers.
User meta (email sign-in code only): a hash of the outstanding code, when it was sent and the wrong-code count.
Sign-up record (user meta _wpus_signup, 1.7.0): for an account created through the My Account or wp-login.php registration form, the time, the IP address, the browser (191 characters), the form, whether the address was one client and the mail servers of the email domain.
Options: your settings, the schema version, the digest clock, and the licence key and status.
What is never storedThe password field of a sign-in form, cookies, authentication tokens, nonces and request bodies. XML-RPC arguments are never logged (only the method name). Credential-shaped keys in a log context are replaced by "[redacted]". A username that does not exist is not stored by name in the attempts table. Exceptions: text typed into the username box that contains an @ is treated as an email address and stored in the attempts table as typed (lower-cased, first 100 characters), whether or not it is a real address; and, while Names typed in failed logins is ticked (the default), the name typed into a failed sign-in is kept in the log row whether or not it contains an @. A visitor can type anything into the username box, a password by mistake included, so a password typed there would be stored. Untick the setting to stop this; names already stored are removed by log retention (or by an erasure request for their email address).
Where it livesIn the WordPress database of the site (five tables with your table prefix and the options above). Time stamps use the site's timezone.
RetentionLog rows: 90 days by default (30, 60, 90, 180, 365 days or forever). Attempt rows: 30 days by default (1 to 3,650, never shorter than the counting window). A daily job deletes older rows. Blocks, allowlist entries, remembered browsers (until they expire) and your settings are never touched by retention. Exports you download and emails the plugin sends are copies outside the plugin's control.
CookiesOne, and only when the email sign-in code is on: wpus_device_<user id>, set after a person enters a correct code. It holds a random token that lets that browser skip the code. Lifetime: Remember a browser for days (30 by default, 1 to 365). HttpOnly, SameSite=Lax, Secure on HTTPS, path and domain from WordPress's cookie settings. The plugin sets no other cookie, runs no tracking, and sets nothing for ordinary visitors. The sign-in notice and every admin screen are produced on the server with no cookie of their own.
Data sent to wpexpertshub.comOnly by the bundled licence and update client, from wp-admin, WordPress cron (the scheduled update check) and WP-CLI, never on a normal page view, a wp-login.php sign-in, an XML-RPC call or a REST request. It sends, over HTTPS to https://wpexpertshub.com/wp-json/wphub-licence/v1/:
Activating a licence (Plugins → WpExperts Hub Licences → Activate licence): the plugin's slug (the fixed value wp-user-security-guard), the licence key and your site address (site_url()).
Deactivating a licence: the same three values.
Email it to me: the plugin's slug and the email address you type.
Update check: your site address, and for each WpExperts Hub plugin on the site that bundles the client, its slug, its licence key (empty when none) and its installed version. It runs whenever WordPress refreshes its plugin-update list (its own twice-daily check, and when you open Dashboard → Updates or the Plugins screen and a refresh is due). The answer is cached for 12 hours, or 1 hour after a failed attempt, and the cache is cleared when you activate or deactivate a licence or run an update.
WordPress's HTTP API adds its usual User-Agent header (the WordPress version and your site address), and the server sees your server's IP address as for any request. No sign-in attempt, IP address, username, block, log row or setting is ever sent. The request waits at most 20 seconds, and if the server cannot be reached the plugin keeps the information it had. No protection feature depends on the licence.
Other data leaving the site Email. Alerts and the digest go to the notification address, and sign-in codes and the test email to the account's address, through wp_mail(), so they pass through whatever mail service your site uses. Alerts contain account names, roles, IP addresses and reasons; they never contain a password or a code (a code is only in the code email itself).
DNS lookup. When an administrator opens User Security → Settings (or Tools, while proxy trust is off), the plugin asks the server's DNS resolver for the host name of that administrator's own IP address (PHP's gethostbyaddr()) to notice a proxy or CDN. The result is not stored. Visitors' addresses are never looked up.
DNS lookups of email domains (Sign-up protection, 1.7.0). When a sign-up is sent through one of the two registration forms, the plugin asks your server's DNS resolver for the mail servers (MX) and addresses (A, AAAA) of the email domain, and once for a random name under its parent domain. The answers are kept for 12 hours (10 minutes after an error). Your resolver, and the DNS servers it asks, see the domain name, never the email address. Never on a page view.
There is no tracking, no analytics, no online address lookup and no blocklist feed. The countries on the Dashboard (1.6.0) come from a file that ships inside the plugin (data/country-ranges.dat, the free IP to Country Lite database by DB-IP.com, CC BY 4.0), read on your own server: no address leaves the site and nothing is stored for them.
DownloadsThe CSV exports contain IP addresses, usernames, emails and user agents of people who tried to sign in. The settings JSON contains your notification email address. Every download needs the managing capability and is recorded in the security log.
Privacy-policy textThe plugin offers suggested text under Settings → Privacy → Policy Guide ("Security log"). It says what is recorded, that the password field and tokens are not (and that a name typed into a failed sign-in is), how long the log is kept, and that a block is not removed by an erasure request (a wide range of addresses that the plugin blocked by itself lifts after the hours chosen in the settings).
Exporter and eraserSince 1.7.0 the export of an account also holds its "Sign-up record", and erasing removes it. "Security log" appears in Tools → Export Personal Data and Tools → Erase Personal Data. It finds rows by email address. Erasing removes the matching log rows and, on the first pass, the attempt rows (unless the filter wpus_erase_attempts returns false). Blocks and allowlist entries are kept. The erasure is logged without the email address.
Sign-in noticeOptional. Tells visitors that their IP address and browser details are recorded, with a link to your privacy policy. See Privacy tools.
DeactivationRemoves the two scheduled events and deletes nothing. The tables, settings, capability and remembered browsers stay.
Uninstall (deleting the plugin) With Delete plugin data on uninstall ticked (the default) deleting the plugin removes: the five tables; the sign-up record (_wpus_signup) of every account; the options wpus_settings, wpus_db_version and wpus_digest_last; the transient wpus_allowlist_cache and every other transient whose name starts with wpus_ (with its timeout entry); the capability wpus_manage_security from every role; and the user meta wpus_2fa_pending, wpus_2fa_sent and wpus_2fa_tries. On multisite this is done for every site, using each site's own setting.
With it unticked nothing is removed, not even the settings, so a reinstall picks up where you left off.
Not removed either way: the licence options _wp-user-security-guard_licence_key and _wp-user-security-guard_key_status and the site transient wpxh_licence_check_v2 (deactivate your licence first if you want them gone), the cookies already in browsers, and files you downloaded.

10. Troubleshooting

ProblemLikely cause and fix
You are locked out because your account or address is blocked.Use any of: WordPress's "Lost your password?" (never blocked); another administrator's session (User Security → Blocked Users or User Security → Blocked IPs, then Unblock; or add your address to the Allowlist and unblock it); or WP-CLI: wp wpus blocks, then wp wpus unblock --account=<login> or wp wpus unblock --ip=<address>. With only file access, renaming the plugin folder stops WordPress loading it (WordPress 7.1.2 skips active plugins whose files are missing and deactivates them when the Plugins screen opens); that removes all protection until you put it back.
A correct password is refused with "The username or password you entered is incorrect."Blocked and wrong-password answers look the same by design. Check User Security → Blocked IPs and User Security → Blocked Users for the address and account, and filter User Security → Security Logs by the username. A refused country or disabled XML-RPC authentication gives the same message; the log reason says which. (A sign-in-code problem has its own messages.)
You allowlisted an address but it is still refused.An allowlist entry stops new automatic blocks and lets the address through a block on the network or range around it. It does not lift a block of the address itself or of its account, so unblock the address (and the account) as well. A wrong password is still a wrong password, and the allowlist does not skip the email code.
You are locked out by the email sign-in code (the code never arrives).Sign in as another administrator and untick Ask for a sign-in code, or run wp wpus two-factor --disable. Fix site email first (an SMTP plugin usually does it) and use Send me a test email before turning the code on again. wp wpus two-factor --user=<login> --revoke forgets one person's remembered browsers.
The code is asked again and again on the same computer.The remembered-browser cookie was blocked or deleted, the password was changed or reset (that forgets the browser), the trust period ended, or the browser's User-Agent string changed (the token is bound to it, so a browser update can trigger a new code). Another device is a different browser.
A page-builder or membership login box stops working for editors after you turned the code on.That form cannot show a code box, so the person is told to finish at wp-login.php. They only need to do it once per browser. Exempt an account with the wpus_two_factor_applies filter, untick Every sign-in form if a social-login plugin conflicts, or switch the code off.
The country rule locked you out.Run wp wpus country --off or allowlist your address from another session. Saving a rule that would refuse your own request is switched back off with an explanation, but only when your request carries the country header.
Every visitor appears with the same IP address, or all failures pile up on one address.The site is behind a proxy or CDN. Turn on User Security → Settings → Reverse proxies → Trust proxy headers and choose the header your proxy sets, then check User Security → Tools → Sign-in diagnostics. Only trust a header your proxy overwrites.
Exploit probing, default-username blocking, the per-IP limit, the rotating-address rules or trusted administrator addresses do nothing.The address is not "one identifiable client": it is private or loopback, or the request carries a forwarding header while proxy trust is off, or the trusted header's last entry cannot be read. The per-IP limit then only scores, and the rules that block a single client or a network stay quiet. Turn on proxy trust (behind a proxy), or return true from wpus_attributable_ip on a private network you know is not shared.
You see many failed sign-ins in the log, but nothing is blocked.Open the card What the recorded attempts show (Dashboard, or above Security Logs). It says whether any address has reached its limit and how often the busiest one tried. Two patterns explain it: an address that stays under its limit (it is only marked suspicious), and an attack that changes address on every request, which no per-address limit sees. The second is what the rotating-address rule counts: 10 different addresses of one network block the network, 25 spread over three networks block the /20 around them. If the card says a network was left alone, it names the reason (see the next rows). See What the recorded attempts show.
The log or the card says a network was left alone, or that the rotating-address rule is paused.On purpose, to keep real visitors out of a block. A network or range is left alone when a trusted address or an Allowlist entry lies inside it, when somebody signed in successfully from inside it in the last 90 days (a sign-in from an address that is itself attacking does not count), or when the address that triggered the check is not one visitor (a proxy edge, a private address). The rule is paused while the successful sign-ins of customers are not logged: tick Settings → Logging → Successful logins (and keep Logging on). You can always block a network or range by hand from the card or from Blocked IPs.
A customer on a blocked network or range cannot sign in.Unblock the network or range on User Security → Blocked IPs (Unblock, or wp wpus unblock --ip=178.239.192.0/20); unblocking also restarts the count of every overlapping network and range. A wide range the plugin blocked lifts by itself after 48 hours. To keep the plugin from ever refusing a customer's office or home network, add it to the Allowlist: an address on the allowlist is never refused because of a network or range block.
The manual form says "Your own IP address is inside that network".The plugin will not block a network or range that holds the address you are browsing from, whatever its size, because that would lock you out. Block a smaller network, or do it from another connection.
An administrator account is never blocked, even under attack.By design. Automatically block administrator accounts is off, so the plugin blocks the attacking address instead and sends the "Administrator block refused" alert. Check User Security → Blocked IPs.
Your local or staging site blocked its own address (127.0.0.1).Failed-login limits apply to every address. Run wp wpus unblock --ip=127.0.0.1. Allowlisting it prevents new blocks, but unblock first.
Alerts, the digest or sign-in codes never arrive.WordPress cannot send email: install an SMTP plugin and use Send me a test email. Also check the master switch on User Security → Emails, the alert's own switch, the notification address, the repeat window (one email per type and subject), and for the digest that the schedule is not Never.
The weekly digest has not arrived yet.The first digest arrives one full period after the daily job first sees the schedule switched on (six hours earlier at most). It needs WP-Cron. Use Send a digest now on Tools or wp wpus digest --send to see one straight away.
Tools warns "WP cron is disabled or overdue, so log retention will not run automatically."DISABLE_WP_CRON is set without a real cron job, or the daily event is missing. Run a real cron that calls wp-cron.php, or run wp wpus retention and wp wpus release yourself. Deactivating and re-activating the plugin re-creates the daily event.
Blocks do not lift after the number of hours you set.Only automatic, non-permanent blocks lift. Blocks made by hand and permanent blocks never do, emergency mode pauses release (and gives new blocks no expiry), and the setting must be above 0. A wide range blocked by the rotating-address rule has its own number of hours (Settings → Login protection → Release a wide-range block by itself after, 48 by default). An expired block stops being enforced at once; its row is tidied by the hourly job (wp wpus release runs it now).
Jetpack, the WordPress mobile app or another XML-RPC client stopped working.XML-RPC is blocked by default. Untick Block XML-RPC and tick Allow XML-RPC authentication only if you need password logins over it.
Settings says "Emergency mode is on, so the values below are overridden".Switch it off on User Security → Tools. The form shows what you saved, and saving never stores the emergency values.
Settings or Tools says "The security tables are not installed."Deactivate and activate the plugin again; activation creates the tables. (The message also mentions a schema installer for WP-CLI, which this plugin does not have.)
A warning names another login-limiting plugin.Keep one. Two plugins that count the same failed sign-ins and lock addresses can lock the wrong person out and double the alerts.
A warning says callbacks run after this plugin's final check.Another plugin or theme hooks authenticate later than priority 100 and could overrule a refusal. Open User Security → Tools → Sign-in diagnostics to see which. It is a hint, not a verdict; a callback that only adds a cookie is harmless.
The sign-in notice does not appear.It is off by default. It is printed by the standard hooks only, so it does not appear on wp_login_form() embeds or fully custom AJAX forms.
A CSV ends with "# Export stopped at the row limit".The file holds at most 100,000 rows, oldest first. Download a shorter range (Last 30 days for the log, or wp wpus export logs --days=N).
Import settings refuses the file.The box was empty ("Paste the exported settings first."), or the file is larger than 200,000 bytes, not valid JSON, not an export from this plugin, from a newer format, or holds no setting this site can use; every one of those answers except the empty-box one says "Nothing was imported." The thirteen settings that are never imported are not an error: they are skipped.
The User Security menu is missing.The signed-in user lacks wpus_manage_security (roles created after activation do not get it), or WPUS_MANAGE_CAP or the wpus_manage_capability filter requires another capability.
The Dashboard chart and event tiles are empty.They are built from the security log. Check that Logging and the event switches are on and that the log retention has not removed the rows.
A real customer says the shop would not create their account.Open User Security → Security Logs, click Sign-ups and find their attempt: the reason names the check. Then fix that check for them: add their address to the allowlist (it passes every check), raise versions behind, lower Minimum time to fill in the form, or delete a line from the two block lists that should not be there. A DNS failure never refuses, so "cannot receive mail" means the domain really has no mail server.
Every sign-up is refused with "Please enable JavaScript".The page did not run the plugin's small script: the theme never prints the footer (wp_footer()), a script optimiser dropped or delayed assets/js/signup.js, or the page was cached for longer than A form stays valid for (hours) (24). Fix the cause, or untick JavaScript check under Settings → Sign-up protection: the other checks stay.
You marked an account as fake by mistake.The account is deleted and cannot be brought back, but what was learned can be: unblock the address under Blocked IPs and delete the domain and the mail-server lines under Settings → Sign-up protection.
Fake accounts still get through.Use Mark as fake on each: the next one from the same domain or mail server is refused. Check that the alert switch, the log and the checks are on, and look at the Sign-ups kind of the log to see what the accepted ones have in common (one network: block it under Blocked IPs).

11. FAQ

Will it lock me out of my own site?

Not by default. Administrator accounts are never blocked automatically unless you switch that on, and allowlisting your own address prevents new automatic blocks. Password reset is never blocked. If you are locked out anyway, wp wpus unblock --account=<login> is the way back in. The one feature that can refuse a correct password is the email sign-in code, which is off by default and has a test-email button.

How is it different from an all-in-one security plugin?

It does one job: sign-in security. There is no file scanning, no firewall and no hardening score. Pair it with a malware scanner if you need that too.

Does it slow my site down?

On ordinary page views the plugin does not query its own tables. Its queries run on sign-ins, XML-RPC and REST authentication, application-password requests and the few 404s that match an exploit path. Rate counters live in transients and the allowlist is cached for 10 minutes.

Which failed attempt blocks an address?

The one that reaches the limit. The failed attempt is written down first and judged second, so with Block after failed attempts from one IP at 10, the 10th failed sign-in inside the counting window blocks the address. (Before version 1.1.0 the engine judged first and every block landed one failure late.)

I see many failed sign-ins in the log, but nothing is blocked. Is something wrong?

Open the card What the recorded attempts show on the Dashboard or above the Security Logs: it counts the addresses and shows how often the busiest one tried compared with the limit that blocks an address. Two patterns explain it. Failures from many addresses, a few from each, never reach a per-address limit; that is what the rotating-address rule is for (10 different addresses of one network block the network, and 25 spread over three networks block the range around them). And an address that stays below its limit (say 6 of 10) is only marked suspicious. If the card says a network was left alone, it names why: customers sign in from it, a trusted address is inside it, or the sign-ins of customers are not logged. See What the recorded attempts show.

What if an attacker changes address on every request?

Then no address reaches a limit, so the plugin counts different addresses per network instead. When 10 different addresses of one IPv4 /24 (IPv6 /64) are refused or try names that are not accounts within the counting window, the whole network is blocked. A bot that also hops between neighbouring networks is caught one level up: 25 addresses spread over at least three /24 networks block the /20 around them (IPv6: /48), and, if you switch it on, 60 spread over three /20 ranges block the /16 (IPv6: /32). The numbers follow Block a network that attacks from many addresses (2.5 and 6 times it). See Rotating addresses and ranges.

Can a network or range block lock out my customers?

It refuses sign-ins from that network or range, never browsing or guest checkout. Before blocking, the plugin leaves a network alone when somebody signed in successfully from it in the last 90 days, when a trusted address or Allowlist entry is inside it, or when the sign-ins of customers are not logged (then the rule is paused). A wide range also lifts by itself after 48 hours, you can unblock it at any moment, and an address on your Allowlist is never refused because of it. A single successful sign-in from anywhere inside a range protects the whole range for 90 days, so a range your customers use is very unlikely to be blocked by mistake.

Why does the log say a network was "not blocked"?

The network crossed its threshold, but one of the safeguards said no and the plugin wrote that down instead of blocking (once an hour per network): an Allowlist or trusted address is inside it, somebody signed in from it in the last 90 days, or the successful sign-ins of customers are not logged. The action wpus_network_left_alone fires too. If you want it blocked anyway, use Block network or Block range on the summary card or the manual form.

What does "suspicious" do?

It writes a "Suspicious authentication activity" log entry (at most one per address and username every 10 minutes) and fires wpus_suspicious_activity. It does not block anything and does not send an email, except for the distributed-attack shape.

Why is XML-RPC blocked by default?

It is a legacy endpoint that amplifies credential attacks and almost no site needs it. If Jetpack, the mobile app or another tool uses it, untick Block XML-RPC and, only if you need password logins over it, tick Allow XML-RPC authentication.

Can bot signals block a real visitor?

No. All bot signals together add at most 35 points and the block score is 80, so a normal browser with normal headers is never blocked on bot grounds alone. (If you lower the block score to 35 or less, bot signals alone can block.)

Does it work behind Cloudflare or a reverse proxy?

Yes, but you must tell it. Turn on Trust proxy headers and choose the header your proxy sets (CF-Connecting-IP for Cloudflare). It is off by default because those headers are easy to fake when no proxy overwrites them. User Security → Tools → Sign-in diagnostics shows which address the plugin sees for you.

Do blocks lift on their own?

Not by default, with one exception. You can opt in to automatic release after a number of hours; then only automatic, non-permanent blocks lift. Blocks you make by hand and permanent blocks never lift by themselves. The exception is a wide range (an IPv4 /20 or /16, an IPv6 /48 or /32) that the plugin blocked by itself: it lifts after 48 hours (Release a wide-range block by itself after; 0 keeps it until you unblock it), even when automatic release is off, because a wrong guess there refuses thousands of people.

Can an address that keeps coming back be blocked for longer?

Yes, with automatic release on. Set Longer every time (2 to 10): each repeat automatic block lasts that many times longer than the last, never more than 8,760 hours. A block you lifted by hand, or a quiet spell longer than Remember it for, starts again at the base length.

Does it protect the WooCommerce login form?

Yes, and it detects WooCommerce by itself. Wrong passwords on My Account and the checkout login are counted, scored and blocked like wp-login.php, and the form is treated as a sign-in page for request bursts, browser-header checks, early refusal, the email code and the sign-in notice. Browsing the shop is never scored. Untick Protect the shop sign-in form to switch it off.

Does it protect the REST API and application passwords?

Yes. A blocked address or account cannot authenticate with an application password, and every REST credential failure gets the same generic 401 answer. Only requests that carry credentials are looked at; the REST API is not blocked as a whole.

How does the email sign-in code work, and is it a full second factor?

It is optional and off by default. For the roles you tick (and every administrator), a sign-in from a browser the plugin does not remember is refused until a six-digit code, emailed to the account, is entered. The browser is then remembered for 30 days by default. It is an email code only: there is no authenticator app, and it is only as safe as the mailbox. Use the test email button first, and know wp wpus two-factor --disable.

Does the exploit-probing rule block my visitors?

No. It looks only at requests WordPress already answered with a 404 for a known probe path, never changes what the visitor sees, and its only consequence is a block on sign-ins from that address. Signed-in editors, allowlisted addresses and shared proxy addresses are ignored.

How do I choose which emails I get, or switch them all off?

Open User Security → Emails (1.5.0). The master switch at the top turns every alert and the digest off; below it every email the plugin can send is listed with its own switch and one line that says when it is sent, together with the address they go to, the repeat window and the accounts to watch. The sign-in code has no switch there because it is part of two-factor authentication: switch that off under Settings instead.

Does it send reports?

One, by default: a weekly security digest to the notification address. It is sent only while email notifications are on, the first one arrives one full period after the daily job first sees it, and you can choose daily, weekly, monthly or never under User Security → Settings → Notifications. Send a digest now on Tools, wp wpus digest and wp wpus digest --send give you one at once.

Can I use it next to Limit Login Attempts Reloaded, Loginizer or Wordfence?

You can, but two plugins that count the same failed sign-ins and lock addresses can lock the wrong person out and double the alerts. Settings and Tools name the other plugin when it is active. Keep one.

Can I use the admin screens on my phone?

Yes, since 1.5.0. The tabs wrap instead of scrolling sideways, the dashboard tiles sit two to a row, the compact tables stack as labelled lines, the list tables use WordPress's own phone layout, and Settings gets a Jump to a section select. Only the 14-day chart still scrolls sideways.

Can I copy my settings from a staging site?

Yes. Download the settings as JSON from Tools on the staging site, paste it into Import settings on the other site, or run wp wpus import-settings. Thirteen settings are never imported (the proxy settings, the notification address, the country rule, the sign-in code switch, emergency mode, blocking of administrator accounts, trusted administrator addresses, automatic release of blocks with its multiplier, and the uninstall choice). The result lists every setting that changed.

I was told the sign-in form had expired. What happened?

Every sign-in form the plugin knows carries a hidden token that is valid for a day (Settings → Sign-in form check). Your form was open for longer than that, or the page came from a copy made before the plugin was switched on. Reload the page and sign in again. Bots that post credential lists without ever loading the form get the same message, which is the point.

Will the form check break my custom login form?

Not if it is one of the usual kinds: wp-login.php, a form made with wp_login_form() (most themes and page builders), the WooCommerce My Account and checkout forms, or a plugin that signs people in on its own page (a post that is not the wp-login.php or WooCommerce form shape is never asked for a token). A hand-made form that posts straight to wp-login.php without WordPress's login_form hook is the one case: exempt it with the wpus_form_origin_required filter, or switch the check off.

Will any of this affect my search ranking?

No. Nothing the plugin does touches a page a search engine reads: refusals happen on wp-login.php, the shop's sign-in form and xmlrpc.php, which crawlers never post to. A visitor's ?author=1 gets the ordinary 404 page while author pages at their real addresses stay as they are, a real crawler is recognised by DNS and never counted by the exploit-probing rule, and the plugin never answers 403 for a page, a 404 or a public REST route. The plugin's test-suite checks that over real HTTP.

Where do the countries on the Dashboard come from?

From a small file that ships with the plugin (about 5 MB, the free IP to Country Lite database by DB-IP.com, credited under the card). Each address is looked up in it on your own server, so nothing is downloaded or sent anywhere, and attempts recorded before 1.6.0 get a country too. It is a display only: the optional country rule still reads only the header your CDN or host adds. Private addresses and addresses the file does not know are listed as "Unknown or private network", and a country is only as exact as such a database can be (a VPN or a mobile network can show another country). The file is refreshed with plugin updates. To use your own source, filter wpus_country_for_ip.

Does it stop fake customer accounts?

Yes, since 1.7.0. Sign-up protection checks the WooCommerce My Account and the wp-login.php registration forms: an account-creation tool that does not run the page's JavaScript, sends the form in a second, fills in a hidden field, uses a throw-away email domain or a browser years out of date, or creates many accounts from one address is refused, even when it solved the CAPTCHA. A fake account that still gets through can be marked as fake on the Users screen, which blocks what it came from and deletes it. See Sign-up protection.

Can it refuse a real customer's sign-up?

Rarely, and it says why. Every refusal is in the Security Logs (kind Sign-ups) with the check that refused it, and the person sees the form again with a message to reload and try again. A failed DNS lookup never refuses, Safari and unknown browsers are never judged, and an allowlisted address passes every check. Accounts made at checkout are never checked.

What does Mark as fake learn, and can I undo it?

The address the account signed up from (an IP block), the mail servers its email domain runs itself and the domain (two lists under Settings → Sign-up protection), then the account is deleted. It never learns Gmail, Outlook and the other free providers, a shared mail server or your own address, and it leaves accounts with orders or content and watched roles alone. The deletion cannot be undone; what was learned can: unblock the address and delete the lines.

Does it send data anywhere?

No sign-in data, IP address or username leaves your site. The one outside contact is the WpExperts Hub licence and update check with wpexpertshub.com. Sign-up protection asks your own server's DNS resolver about the email domain of a sign-up (never the address itself). See Privacy for exactly what it sends and when. The plugin also asks your server's DNS resolver for the host name of an administrator's own address when Settings or Tools is opened.

What happens when I delete the plugin?

Deactivating deletes nothing. Deleting removes the plugin's tables, options, transients, user meta and capability only when Delete plugin data on uninstall is ticked (it is by default). Untick it first to keep your block list and logs. The licence options are not removed.

How do I activate my licence and get updates?

Your key is emailed after purchase and shown under My Account → Downloads. Open Plugins → WpExperts Hub Licences, paste it and click Activate licence. New versions then appear on Dashboard → Updates. The plugin works without a licence; the key only unlocks updates. You can also upload a new zip and choose Replace current with uploaded; your settings and data are kept.

Does a block log people out?

No. A block stops new sign-ins. Cookies that were already issued keep working until they expire.

Where do I see why something was blocked?

On the block lists (the Reason column), in User Security → Security Logs (the Reason and the expandable detail), and in the alert email.

Can another role manage the plugin?

Grant the capability wpus_manage_security to the role with a role editor, or define WPUS_MANAGE_CAP in wp-config.php (for example define( 'WPUS_MANAGE_CAP', 'manage_options' );) or use the wpus_manage_capability filter.

12. Changelog

1.7.0 - 2026-10-26

  • New: Sign-up protection for the WooCommerce My Account registration form and wp-login.php?action=register (Settings → Sign-up protection, on by default), built after fake customer accounts kept appearing on a store whose CAPTCHA the bot solved. A block of the address, network or account now stops a sign-up too. The form's token reaches its field only through a small script, so a tool that posts the form without running scripts is refused. A form sent less than 4 seconds after it was shown, a filled hidden field, no browser name, an automation tool, or a Chrome-based browser or Firefox about two years out of date is refused. The email domain must be able to receive mail, may not be a throw-away domain that answers for any made-up name (a wildcard MX), and may not use a mail server or be a domain on your block lists. More than 3 accepted sign-ups from one address in an hour are refused, and an address whose sign-ups keep being refused is blocked. An account that probes xmlrpc.php within an hour of signing up is blocked (never deleted). Checkout, the Store API, the admin, REST, WP-CLI and imports are never checked; a refused person sees the form again with one message, never a 403; a DNS error never refuses.
  • New: Mark as fake on the Users screen (a row link and a bulk action), under an accepted sign-up in the Security Logs, and wp wpus mark-fake <user>. After a confirmation it blocks the account's sign-up address, adds the mail servers its email domain runs itself and the domain to the sign-up block lists, and deletes the account. It never learns a free mail provider, a shared mail server, an allowlisted, private or CDN address or your own address, and leaves an account with orders, content or a watched role alone.
  • New: every account created through one of the two forms keeps a short sign-up record (time, address, browser, form, the domain's mail servers), in the privacy export and erasure and removed by uninstall.
  • New: a "Sign-ups" kind in the Security Logs, wp wpus signups, a Sign-ups section in the security digest, refused sign-ups on the Dashboard, and an optional alert "A sign-up was refused" on the Emails screen (off by default).
  • New: on a sign-in, a browser years out of date adds a small bot signal (15 points, inside the existing cap: it never blocks a sign-in alone).
  • New: filters wpus_registration_required, wpus_registration_refused_message, wpus_dns_lookup, wpus_browser_current_major, wpus_never_learn_domains, wpus_wildcard_mx_exempt; actions wpus_registration_refused, wpus_registration_recorded, wpus_user_marked_fake.
  • Changed: the security digest's sign-in figures no longer count refused sign-ups; they have their own section.

1.6.0 - 2026-10-16

  • New: Who is knocking? Three cards on the Dashboard. Login attempts lists the latest successful and failed sign-ins (switch between them) with the username typed, the address with its country flag and how long ago. Top countries by number of attacks shows where the last 7 days of failed and refused sign-ins came from (bar, attempts, addresses, share). Top IPs blocked switches between the last 24 hours, 7 days and 30 days and lists the addresses and networks blocked, most attempts first, with their status and a link to Blocked IPs.
  • New: countries are looked up offline, in a small file that ships with the plugin (the free IP to Country Lite database by DB-IP.com, CC BY 4.0, credited under the card). Nothing is downloaded and no address is sent anywhere, and earlier attempts get a country too. It is a display only; the country rule still reads only your CDN's header. The filter wpus_country_for_ip replaces the answer.
  • Improved: on a phone the new lists show every row as two short lines instead of a table that scrolls sideways.
  • Changed: the texts that said there is "no GeoIP database" now say that the country rule uses none.

1.5.0 - 2026-10-15

  • New: Emails screen (User Security → Emails). Every email the plugin can send on one screen, with a switch for each: the master switch, the address they go to, the eight security alerts (each explained in a line), the repeat window, the accounts to watch, the security digest with its schedule and last sent / next due, a Send a digest now button, and the emails that go to the person signing in (the sign-in code, with a link to its switch, and the test email). The Notifications card on Settings now points there; the links at the foot of every alert email open it. Nothing moved in the database.
  • Fixed: the admin screens on a phone. The tab bar scrolled sideways and hid the current screen's tab: the tabs now wrap. The compact tables of the Dashboard and the activity summary were 540px wide inside a 390px screen: their rows now stack as labelled lines. The long list of Settings sections becomes a Jump to a section select on a phone, the dashboard tiles are more compact, and the system-information boxes no longer have a minimum width wider than the screen.
  • Fixed: a save of the Emails form leaves every other setting alone, and a save of the Settings form leaves the email settings alone (each form only writes the keys it shows).

1.4.0 - 2026-10-14

  • New: Sign-in form check. Every sign-in form (wp-login.php, wp_login_form(), the WooCommerce form) carries a hidden, signed, time-limited token. A sign-in post without a valid one never loaded the form: it is refused before the password is looked at, with its own "reload and try again" message, and recorded as "form not loaded" rather than a failed sign-in. An address that keeps posting without the form is blocked at the failed-sign-in limit; the refusals count for the rotating-address rule. On by default, 24-hour lifetime; filters wpus_form_origin_required, wpus_form_origin_lifetime, wpus_form_origin_message.
  • New: Distributed attacks. When at least 8 different addresses fail with names that are not accounts, or post the form without loading it, within 60 minutes, the site is under attack for 60 minutes (extended while it goes on): per-address limits are halved (never below two tries), a network is blocked at half the usual number of addresses, the Dashboard and Security Logs say so, and one email is sent. wp wpus spray shows or ends it. A customer's wrong password on a real account never starts it.
  • New: Account discovery closed. The REST users list is a missing route for visitors who are not signed in; a visitor's ?author=N is the 404 page (and counts as a probe); oEmbed answers name no author. Author pages at their normal addresses are untouched.
  • New: Search-engine crawlers verified by reverse-then-forward DNS (cached a day, only when a client claims to be one). A verified crawler is never counted by the exploit-probing rule; a fake one gets no exemption and the log says "claims to be Googlebot (not verified)". Filter wpus_known_crawlers.
  • New: the Security Logs have a second row of one-click filters by kind (sign-ins, blocks, XML-RPC, bots and probes, administration).
  • New: a request from a CDN edge address (Cloudflare's published ranges) is never treated as one client, whichever proxy header is chosen. Filter wpus_shared_edge_ranges.
  • New: saving Settings with a proxy header the saving request did not carry shows a warning naming the headers it did carry; the same advice appears on the Settings and Tools screens.
  • Changed: a refused XML-RPC request writes one Security Logs line per address per ten minutes (every request is still an attempt row, and every rule still counts every one of them).
  • Changed: a failed sign-in, a refused XML-RPC request and a probe from a client that claims to be a crawler record whether it really is one.

1.3.1 - 2026-10-13

  • Changed: the User Security menu sits right after Users in the admin menu, next to the accounts it protects (it was at the bottom, after Settings).
  • Fixed: on a phone, the number tiles of the security digest were shifted sideways one by one, and stacked numbers stayed right-aligned. They now stack flush and read from the left.

1.3.0 - 2026-10-13

  • New: every email the plugin sends is designed. Security alerts, the security digest, the sign-in code and the Settings test email arrive as a card with the plugin's shield icon (the one in the admin menu) and your site at the top, a coloured label that shows how serious it is (red for an attack, amber for a warning, blue for information, green for all clear), the facts in a table (address, account, role, form, time), what to do next, and a button to the right screen: Blocked IPs or Blocked Users searched for the value, or the Security Logs filtered to that address or account.
  • New: every email also carries a plain-text version with the same words, for mail apps that prefer text. The sign-in code stays on a line of its own there.
  • New: the security digest shows its figures as tiles, the busiest addresses and the most-targeted accounts as ranked lists, and the period in your site's date format.
  • New: the sign-in code email also names the account, the IP address, the browser and the time, and says what to do when it was not you (a code is only sent after the correct password).
  • New for developers: the wpus_email_message filter changes any email before it is laid out, and wpus_email_html returning false sends plain text only. wpus_digest_lines is deprecated but still honoured.
  • Changed: alerts show the risk decision as "Allowed", "Suspicious" or "Blocked"; the digest says "1 account" instead of "1 accounts"; the account-block email names the real menu (User Security → Blocked Users).
  • Unchanged: the subjects (your mail filters keep working) and when an email is sent. Nothing is loaded from a server: the icon travels inside the email.

1.2.0 - 2026-10-12

  • New: range escalation. A bot that changes address on every attempt AND hops between neighbouring networks (178.239.198.x, then 178.239.194.x) used to stay under every limit, because the plugin only grouped addresses by their /24 (IPv6 /64). The evidence is now also counted over the IPv4 /20 around them (IPv6 /48) and, when you switch it on, the /16 (IPv6 /32): 2.5 times the network number (25 by default) for a /20 and six times (60) for a /16, spread over at least three of the narrower networks inside. The narrowest range that shows the attack is blocked.
  • New: a wide range block lifts by itself after 48 hours (Settings → Login protection → "Release a wide-range block by itself after"; 0 keeps it until you unblock it), even when automatic release of other blocks is off, because a wrong guess there refuses thousands of people. A block you make by hand never expires.
  • New: the same safeguards at every size: a trusted address or range inside, a sign-in from anywhere inside the range in the last 90 days, or sign-in logging being off leave the range alone and say so in the log. An address on your allowlist is never refused because of a range block.
  • New: Blocked IPs, the manual block form, the Security Logs address filter and WP-CLI accept a range (wp wpus block --ip=178.239.192.0/20; a member address may be typed); the list marks it "range /20" with its size and countdown, the e-mail names it and says when it lifts, and the activity summary lists a wide range that the attack is spread over, with a one-click "Block range".
  • Changed: an unblock (or a block that ran out) restarts the count of every overlapping range, not only of the very network, so lifting a /20 cannot be undone a moment later by the /24 inside it or the /16 around it. You cannot block a range that holds your own address, whatever its size.
  • Changed: "View activity" and the log's address field match a network, including the numeric edges of a /20 (an IPv6 range whose first four groups contain a zero may show a few rows of its neighbours).
  • Changed (found by an independent review of this release): the "somebody signed in from it" safeguard now needs the sign-ins of CUSTOMERS to be logged (Settings → Logging → Successful logins). With only the administrators' sign-ins logged it believed nobody else used the range; now the rule is paused and says so. This also applies to a single network.
  • Changed: a wide range counts each narrower network for at most as many addresses as it takes to block that network on its own, less one (nine of the 25 by default). One dense network plus two stray addresses elsewhere used to block the whole /20.
  • Fixed: one very dense network (thousands of random IPv6 addresses of one /64) filled the row cap and hid the other attacked networks from the /48 around it; the evidence is read again after setting such a network aside.
  • Fixed: the hourly job could lift a block that had been refreshed or replaced by an administrator's while it was working through its batch (an administrator's block never expires). The release is now one statement that checks the block is still due.
  • Fixed: a block reason longer than its column (a translation of the range text) is cut instead of making a strict MySQL server refuse the whole block.
  • Fixed: a blank, negative or non-numeric "Release a wide-range block after" field saved 0 ("never"); it now keeps the stored value.
  • Fixed: the e-mail about a range or network an administrator blocked by hand no longer claims that an attack was seen.
  • Changed (second review): a sign-in from an address that is itself among the attacking addresses no longer makes its range look like a customers' network: a bot that signs in with a stolen password from one of its own addresses used to switch the automatic wide block off for its whole range for 90 days.
  • Fixed (second review): the hourly job released the partner rows of an account block (username, e-mail, user id) all together, a row an administrator had made permanent included. Each row is now released on its own terms: one statement that checks that the row is still due.
  • Fixed (second review): emergency mode could make a wide range HARDER to block than normal mode (the counting caps followed the lowered limit). The caps now come from the saved limit, so emergency mode only ever blocks earlier.
  • Fixed (second review): the first /64 of every IPv6 /48 (2001:db8:1::/64) could not be set aside when it crowded the evidence of its /48; both ways its addresses are written are now recognised.
  • Improved (second review): the release floor is read once per decision and the cheap look is reused when there is no floor; the "paused" notices name the "Successful logins" switch; the card says how a busy network counts.
  • Improved: a refused request costs fewer and cheaper queries (the evidence is read once before anything more is done; the release floor reads only the blocks that can overlap, instead of every block ever made); a range that is left alone is noted once, for the narrowest range, not once per wider range.

1.1.0 - 2026-10-12

  • Fixed: an address that kept guessing names that are not accounts was never blocked, however often it tried. Reaching "Block after failed attempts from one IP" now blocks the address (an address the plugin can tell apart from other visitors; behind a proxy that is not set up the limit still only scores).
  • Fixed: the failed-login limit was reached one failure late. The failed attempt is now recorded before it is judged, so a limit of 5 blocks on the 5th failure, and the log row carries the score and the counters that include it.
  • Fixed: two real accounts plus a refused request or an unknown name were counted as three accounts targeted from one address.
  • Fixed: the daily job that forgets expired remembered browsers (email sign-in code) failed on MySQL 8 with "Incorrect DATETIME value" and removed nothing; one such error was written to the PHP log every day.
  • Fixed: a scanner that rotates through addresses sent one "Blocked XML-RPC request" email per address (36 in two minutes in the data this release was checked against). There is now one alert per throttle window for the whole site, naming the latest address and how many requests were held back since the last one.
  • New: rotating-address protection. When 10 different addresses of one network (an IPv4 /24, an IPv6 /64) are refused or try names that are not accounts within the counting window, the whole network is blocked (Settings → Login protection; switch it off or change the number, 3 to 500). Failed passwords for real accounts never count; a network that overlaps your allowlist or that somebody signed in from in the last 90 days is left alone and reported in the log. Emergency mode caps the number at 5.
  • New: Blocked IPs, the manual block form and WP-CLI (wp wpus block --ip=203.0.113.0/24) take a network; the lists mark it and show how many addresses it covers.
  • New: "What the recorded attempts show" on the dashboard and the Security Logs: addresses, the busiest address against the limit, blocks, a plain sentence about the pattern, the networks that used many addresses, and a one-click Block network.
  • Improved: Security Logs: compact rows (the client as "Chrome on Windows" or "curl", the error code as a label, day and time on two lines), outcome filters with counts, a Block address link on refused rows, "Blocked" / "Network blocked" marks, and a detail panel that shows what the counters were next to the limits instead of the stored JSON.
  • Improved: every screen opens with a plain title and the same pill tab bar as the other WpExperts Hub plugins (no header card).
  • Changed: the Settings text for the per-address limit says that it blocks.
  • Fixed: with "Trust proxy headers" on, an unreadable LAST entry of the header ("unknown", a hidden entry) made the entry before it, which the visitor wrote, the client's address. Only the last entry counts now, a port after it ("198.51.100.7:443") is ignored, and a header that gives nothing usable means the request is not treated as one identifiable client.
  • Fixed: requests that were turned away because an account or a whole network was blocked counted toward the address's failed-login limit, so the first typo after you lifted a block could block that customer's own address again. Only failed sign-ins count toward the limit.
  • Fixed: the safeguard "somebody signed in from this network" read a list of at most 2000 addresses and could miss the network in question on a busy shop. It now asks about that one network. With successful sign-ins not logged at all, the rotating-address rule says it is paused instead of blocking networks it cannot judge.
  • Fixed: an address on your allowlist was still refused because of the block on its network. The allowlist now lets it through (an exact block of the address itself is still lifted with Unblock).
  • Fixed: the one-click "Block network" was offered for addresses of a proxy or CDN the plugin was not told about, and for private ranges. It is not any more, and says why.
  • Fixed: the summary sentences compared a total over days with a limit that counts failed sign-ins within minutes. They now say what is known and what is not.
  • Fixed: "View activity" on a network searched for the text anywhere (45.133.7. also matched 145.133.7.5); the Security Logs address field now accepts a network such as 203.0.113.0/24 and matches its addresses exactly. "0.0.0.5/24" is not accepted as a network, and a settings file that switches the network rule off is reported like the other protections.

1.0.2 - 2026-10-11

  • New: Security Logs now show the username or email address that was typed in a failed sign-in, even when no such account exists, so you can see what attackers are guessing. Names are shown as plain text, can be searched, and are included in the CSV export (column "attempted_name"). Counters and blocking are unchanged.
  • New setting: Settings → Logging → "Names typed in failed logins" (on by default). Turn it off to stop recording typed names; names already stored are kept until log retention removes them.

1.0.1 - 2026-10-08

  • Fixed: on the dashboard, "Latest security events" squeezed long IPv6 addresses into a column one character wide, so every row became very tall. The account (or email) and the IP address now share one "Account / IP" column, with the address underneath.
  • Improved: IP addresses in Security Logs, Blocked IPs, the Allowlist and the dashboard are shown in one consistent monospace chip. IPv6 addresses wrap only after a colon, never in the middle of a group, and the full address is in the tooltip.

1.0.0 - 2026-10-02

  • Initial release.
  • Login protection with per-IP, per-username and per-email failed-attempt thresholds, a rolling window and a configurable "suspicious" tier.
  • Distributed-attack detection: many IP addresses attacking one account.
  • Bot and automation detection (frequency buckets, missing browser headers, automation User-Agents, sustained hammering, XML-RPC automation), capped so signals never block a browser on their own.
  • A central, filterable authentication risk engine producing one decision per request: allow, suspicious or block.
  • Administrator protection: stricter thresholds, dedicated admin logs and a "never auto-block an administrator" guard rail.
  • Generic login errors, so failed logins never reveal which usernames exist.
  • XML-RPC protection with a generic 403 and per-IP escalation.
  • Blocked IP and blocked account screens with View, Unblock, Permanent and Delete actions — manual only, never automatic.
  • Allowlist for exact IP addresses, CIDR ranges and 10.0.0.* shorthand.
  • Security log with 20 event types, filtering, redaction and configurable retention.
  • Dashboard, dashboard widget, settings, tools and system report screens.
  • Throttled per-type administrator notifications.
  • Optional automatic release of automatic blocks (off by default), repeat-offender memory, a country rule that uses your CDN's country header, and optional early refusal of blocked addresses.
  • A redesigned admin: one tab bar, status tiles, a 14-day activity chart, section navigation and a sticky save bar on Settings.
  • WooCommerce sign-in form protection (detected automatically), default-username detection, exploit path probing (404s) and optional trusted administrator addresses.
  • The optional email sign-in code now covers every sign-in form (including the WooCommerce form) and a list of roles — administrator, editor, author and shop manager by default — with a safety net for a sign-in that would skip it, a one-code-a-minute limit, a per-account trusted-browser cookie and a test email button.
  • Alerts to the site owner when somebody tries to get into a watched account (roles are configurable), when somebody has the password but fails the code, and (optionally) when a watched account signs in.
  • WordPress privacy tools: suggested policy text plus export and erase for log data.
  • Scheduled security digest (daily, weekly or monthly) with a per-day threat level.
  • Optional longer blocks for an address that keeps coming back after the automatic release.
  • Sign-in diagnostics on Tools: the address the plugin sees, the header to choose behind a CDN, and every other callback or login-limiting plugin on the sign-in.
  • CSV export of the log, blocks and allowlist (formula-safe, with a visible marker if a file is cut at the row limit), JSON export of the settings, and a settings import that never touches the server-specific, lockout-prone or opt-in loosening settings and names everything it changed.
  • Optional privacy notice under the sign-in forms.
  • A block made from WP-CLI (wp wpus block --ip=…) now counts as an administrator's block: it never gets an expiry and can replace an active automatic block.
  • WP-CLI commands (wp wpus), including the documented lockout-recovery unblock, digest, diagnose, export and import-settings.
  • No database work on ordinary page views; every SQL statement lives in one repository class.

13. Support

Email support@wpexpertshub.com and a person will help. Please include:

  • Your order ID (from your purchase email or My Account on wpexpertshub.com).
  • The plugin version (shown in the header of every User Security screen, for example "Version 1.4.0"), and the WordPress and PHP versions (User Security → Tools → System information lists them).
  • What you expected, what happened, and what you already tried. The relevant rows from User Security → Security Logs (the reason and the detail) and the output of wp wpus diagnose help a great deal.
  • Whether the site is behind a CDN or proxy, and whether another security plugin is active.

Please do not email passwords. If you are locked out, try the steps under Troubleshooting first: wp wpus unblock works without a browser session.