User Security Guard for WordPress
Stops brute-force sign-ins and bots with automatic IP and account blocking, a risk engine, a security log, an allowlist, a weekly digest and an optional email sign-in code.
Version 1.0.0 WordPress plugin Paid plugin Requires WordPress 6.2 Requires PHP 7.4 Tested up to 7.1 Updated 6 Oct 2026
1. Overview
User Security Guard for WordPress protects who can sign in to your site. 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. Every block stores a reason you can read, and by default a block lasts until an administrator removes it.
This page documents version 1.0.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 some email texts still say "WP User Security". The admin menu is called User Security.
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 and the sign-in notice) are off until you turn them on.
How it works
- On
wp-login.php,wp-admin, the WooCommerce sign-in forms and XML-RPC the plugin sits on WordPress'sauthenticatefilter (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. - After a wrong password the plugin records the attempt and runs the risk engine. 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.
- 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.
- 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.
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).
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). 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.
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.
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.
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 20 event types, a dashboard with a 14-day chart, allowlist, throttled email alerts, 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, 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 look up countries. The optional country rule reads a header that your CDN or host adds; there is no GeoIP database.
- It does not score ordinary page views, browsing the shop, viewing My Account or checking out.
- It does not group IPv6 addresses. Counters use the exact address.
- 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
| Item | Requirement | Notes |
|---|---|---|
| WordPress | 6.2 or later | From 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. |
| PHP | 7.4 or later | From the plugin header (Requires PHP). The Tools screen marks the PHP version with a "Check" flag below 7.4. |
| Database | MySQL or MariaDB | The plugin creates five tables with WordPress's dbDelta() and uses MySQL-style statements (ON DUPLICATE KEY UPDATE, DELETE ... LIMIT). |
| WooCommerce | Optional | Detected 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. 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 delivery | Needed for some features | The 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. |
| WP-Cron | Needed for housekeeping | Log 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. |
| HTTPS | Recommended | Settings 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 CDN | You must tell the plugin | Behind 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 plugins | Keep one | The 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 plugins | This plugin steps aside | When 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 screens | Capability wpus_manage_security | Granted to the administrator role on activation. See Capability, constant and roles. |
| Multisite | Supported | Tables 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
- Download the plugin zip from your account on wpexpertshub.com (My Account → Downloads).
- In WordPress, open Plugins → Add Plugin → Upload Plugin, choose the zip file and click Install Now.
- 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.
- Open User Security in the admin menu. 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.)
- Open User Security → Settings and read the cards. Behind a CDN or reverse proxy, set Reverse proxies first.
- 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.
- 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. - 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.
- New versions then appear on Dashboard → Updates and on the Plugins screen, and install like any other plugin update.
- 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.
- 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.
What activation creates
| What | Name | Notes |
|---|---|---|
| Database tables | {prefix}wpus_attempts, {prefix}wpus_blocks, {prefix}wpus_logs, {prefix}wpus_allowlist, {prefix}wpus_devices | Created with dbDelta(). Columns are listed under Database tables. |
| Option | wpus_settings | Added only when it does not exist yet, with all 73 default values. Existing settings are never overwritten by activation. |
| Option | wpus_db_version | The installed schema version (1.2.0 in this release). It is written by the schema check that runs on wp_loaded and admin_init. |
| Capability | wpus_manage_security | Added to the administrator role. |
| Scheduled events | wpus_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.
- 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
- Install and activate the plugin. Open User Security → Dashboard and read the warnings, if any.
- 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.
- 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."
- 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.
- Open User Security → Settings → Notifications. Leave Send notifications to empty to use the site administration address, or enter another address. Keep Security digest at Every week or choose another schedule.
- If another login-limiting plugin is active, keep only one. Settings and Tools name the plugin.
- 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 --disableis the way back in. - Activate your licence under Plugins → WpExperts Hub Licences.
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.
- 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. Onwp-login.phpthe 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. - Early refusal (optional, off by default). A blocked address gets a bare 403 before the login page is shown. See Blocks.
authenticateat 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.- WordPress checks the password. In WordPress 7.1.2 core's own password checks run at priority 20.
authenticateat priority 99: the email sign-in code, when it is switched on and the account is covered. See Email sign-in code.authenticateat 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, the risk engine runs, and 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. It counts recent failures and adds points for each signal that applies. The counters are read before the failure that just happened is saved, so a limit of 10 takes effect on the 11th failed attempt: the engine sees ten earlier failures and acts.
| Signal (code) | Points | Applies when (defaults in brackets) |
|---|---|---|
ip_threshold_reached | 60 | Earlier failed or refused attempts from this address inside the counting window reach the per-IP limit (10). This 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_suspicious | 25 | The count reaches the "suspicious" limit (5; 4 when the account is an administrator) but not the per-IP limit. |
multiple_usernames_from_ip | 15 | Five or more different usernames of real accounts were tried from this address. |
multiple_accounts_from_ip | 20 | Three or more different accounts (username and email pairs) were tried from this address. |
probing_activity | 15 | Two 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) | 60 | Failed attempts against this username or email reach the account limit (20; for administrators 5). Asks for an account block. |
account_failures_suspicious | 20 | Failures against this account reach the "suspicious" limit (5) but not the account limit. |
distributed_account_attack | 70 | The 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_targeted | 20 | The account is an administrator and has at least one earlier failure. |
default_username | 15 | A default username such as admin that is not an account on this site was tried. See Default usernames. |
default_username_repeated | 60 | The same address reached the default-username limit (3) and is one identifiable client. Asks for an IP block. |
bot_indicators | up to 35 | Automation signals combined, capped at 35. See Bot detection. |
repeat_offender | 0 | The 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_account | 100 | The address or account is already on the block list. |
The decision. A request is blocked when a rule above asks for a block (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); 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.
Three worked examples with the default settings
- Someone guesses the password of a real, ordinary account from one address with a normal browser. Until five earlier failures exist, nothing happens. From the 6th failed attempt the address scores 25 (suspicious address) plus 20 (suspicious account) and is marked suspicious. On the 11th failed attempt, ten earlier failures exist: 60 plus 20 reaches 80 and the address is blocked, with the reason "Risk score 80 reached the blocking threshold of 80".
- Someone guesses an administrator account. The administrator limits apply (5). On the 6th failed attempt the account limit (60) is reached, the address scores 25 as suspicious (its "suspicious" limit is 4 here; the 60-point per-IP limit 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).
- A script tries many made-up usernames. Unknown usernames are not stored by name, so only the per-IP limit applies: 60 points on the 11th failed attempt. That is below the block score of 80, so a script that looks like a normal browser is marked suspicious, not blocked. A script that announces itself as
curlgets 20 extra points and is blocked on that same attempt (60 plus 20 is 80); together with missing browser headers the bot signals add up to their cap of 35. A default name such asadminadds 15 points per attempt and blocks the address on the third attempt.
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 or account 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 and refused rows from the same IP address inside the counting window. 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 next failed attempt after ten different addresses have already failed triggers it.
- A manual unblock restarts the counters. After you unblock an address or account, only attempts made after the unblock count. 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_refusedand 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_passwordand 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_passwordand 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).
| Signal | Points | When |
|---|---|---|
Automation user agent (automation_user_agent) | 20 | The 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) | 15 | A sign-in POST carries at most one of the four headers Accept, Accept-Language, Accept-Encoding and Connection. |
Few browser headers (thin_browser_headers) | 8 | A sign-in POST carries exactly two of those four. |
No user agent (empty_user_agent) | 10 | A sign-in request has an empty user agent. |
High frequency (high_frequency) | 12 | The 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) | 25 | Four times that rate or more in a minute. |
Sustained hammering (sustained_frequency) | 15 | Six times the per-minute figure or more in the last hour (60 requests by default). |
XML-RPC (xmlrpc_automation) | 10 | The 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 fireswpus_attack_spike, which sends the "A burst of traffic on the login page" alert. A sustained burst of GET requests towp-login.phpis 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,wordpressandadm; the same name followed by up to six digits, with an optional-,_or.in between (for exampleadmin1,admin_2024,root-01); and the part before the@of an email address (admin@example.com). The filterwpus_generic_usernameschanges 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
adminis 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,.sshand similar), copies ofwp-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/phpunitand 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 filterwpus_probe_ruleslets 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.phpis 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'sxmlrpc_enabledfilter. This applies to every address, including allowlisted ones. - Each refused request is logged as "XML-RPC blocked" and, except from an allowlisted address, recorded as a refused attempt. 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" (throttled per address). - 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,usernameandpassword. WooCommerce's nonce is not required, because a bot that leaves it out is still attacking the form. The request is labelled with the endpointwoocommercein 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 theauthenticatefilters. The plugin hooksapplication_password_is_api_requestto 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 (type
ip). Listed on User Security → Blocked IPs. - Account (type
accountfor the username, the email address and the identifier that was typed, and typeuserfor 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. With automatic release on, an automatic block 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). 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 and a reason, tick Flag as a permanent block if you want one (it starts ticked when Allow permanent blocks (IP blocks) is on). 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."), an invalid address ("Enter a valid IPv4 or IPv6 address."), and an allowlisted address ("This IP address is on the allowlist. Remove it from the allowlist before blocking it.").
- 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.
- 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 fireswpus_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 releaseruns the release on demand.
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.
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 shorthand10.*,10.0.*or10.0.0.*(stored as10.0.0.0/8,10.0.0.0/16and10.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. Manual blocking of an allowlisted address 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_iplets 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-CodeandGEOIP_COUNTRY_CODE. Nothing else is ever read. There is no GeoIP database to download. 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 asXX(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).
wp wpus two-factor --disable.How it works for the person signing in
- 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>").
- 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.
- 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 filterwpus_two_factor_appliesexempts 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_activefor 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> --revokeforgets that person's browsers, andwp wpus two-factor --disableswitches the feature off for everyone.
Alerts
Alerts are plain-text emails to the address in Send notifications to (or the site administration address when that is empty), sent with wp_mail(). All alerts need Email notifications ticked and their own switch. 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 carries the heading "User Security Guard — <site name>", the line "This is an automated security notification. Open the security dashboard to review, block or unblock anything.", and a link to the Dashboard.
| Alert | Switch (default) | Subject line | Sent when |
|---|---|---|---|
| A new IP address was blocked | notify_ip_blocked (on) | IP address blocked: <address> | A new IP block is created, automatic or manual. Says which, and the reason. |
| An account was attacked from several IP addresses | notify_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 to sign in to a watched account (wrong password) | notify_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. |
| Somebody has the password of a watched account but fails the sign-in code (or the code cannot be emailed) | notify_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 in (every time; off by default) | notify_privileged_signin (off) | The account "<name>" signed in | A completed interactive sign-in by a watched account (not one the code check voided). Includes address and browser. |
| A burst of traffic on the login page | notify_attack_spike (on) | Login endpoint attack spike from <address> | An address reaches the high-frequency rate (see Bot detection). |
| XML-RPC is being hammered | notify_xmlrpc_attack (on) | Blocked XML-RPC request from <address> | A request to XML-RPC is refused while XML-RPC is blocked. |
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 Settings 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 plain-text email, Every day, Every week (default) or Every month, set with Security digest under User Security → Settings → Notifications (Never switches it off). It follows the Email notifications switch and goes to the notification address. 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 period; the threat level; failed sign-ins; requests refused because the address or account was already blocked; how many different IP addresses were involved; how many accounts that exist on the site were targeted; successful sign-ins (only when logging and "Successful logins" are on); IP addresses and accounts blocked in the period; what is blocked right now; automation detections and refused XML-RPC requests (only when there were any); the five busiest IP addresses; the five most-targeted accounts; 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."
- 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_levelchanges 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 digestprints it. Neither changes the schedule. The digest needs WP-Cron. - The filter
wpus_digest_lineschanges the lines.
Security log
The log has 20 event types. 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) | Label | Written when | Switch |
|---|---|---|---|
login_success | Successful login | A non-administrator signs in (also with an application password). | Successful logins |
login_failed | Failed login | A wrong password for a non-administrator or unknown account. | Failed logins |
login_blocked | Blocked login | A 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_blocked | IP blocked | A new IP block (automatic or manual). | Requests refused by a block |
user_blocked | User blocked | A new account block. | Requests refused by a block |
block_deleted | Block record deleted | An administrator uses Delete on a block. | Requests refused by a block |
suspicious_activity | Suspicious authentication activity | A suspicious result, a refused account block, or the first exploit probe in a window. | Requests refused by a block |
bot_detected | Bot / automation detected | A failed attempt with bot signals, or a burst on the login page. | Bot detections |
xmlrpc_request | XML-RPC request | An allowed XML-RPC call (method name only). | XML-RPC requests (and Log XML-RPC activity) |
xmlrpc_blocked | XML-RPC blocked | A request refused while XML-RPC is blocked. | XML-RPC requests |
admin_login | Administrator login | An account that can manage_options signs in. | Log successful admin logins |
admin_login_failed | Administrator login failure | A wrong password for an administrator account. | Log failed admin logins |
ip_unblocked | IP unblocked (manual) | An address is unblocked by a person, WP-CLI or automatic release. | Always |
user_unblocked | User unblocked (manual) | An account is unblocked. | Always |
settings_changed | Security settings changed | Settings saved or imported, every manual action on the admin screens, and every export. | Always |
allowlist_added, allowlist_removed | Allowlist entry added, Allowlist entry removed | Any change to the allowlist, including automatic entries. | Always |
logs_purged | Security log cleaned up | Retention cleanup, a manual purge, or a privacy erasure. | Always |
emergency_enabled, emergency_disabled | Emergency mode enabled, Emergency mode disabled | Emergency mode is switched. | Always |
- What a row holds: time, event, risk level (Low, Medium, High, Critical) and score, IP address, 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.
- 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,bearerorcredentialis 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 filterwpus_log_datachanges 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.
| Setting | Value while emergency mode is on |
|---|---|
| Suspicious attempts per IP | 1 |
| Block after failed attempts from one IP | 2 |
| Block after failed attempts on one username or one email | 3 |
| Block an account attacked from this many IPs | 3 |
| Failed attempts before an admin account is treated as targeted | 1 |
| Risk score for "block" and for "suspicious" | 50 and 10 |
| Requests per minute that count as high frequency | 5, or your value if lower |
| Counting window (minutes) | at least 60 |
| Default usernames threshold, exploit probing threshold | 2 and 3, or your values if lower; both rules forced on |
| Login protection, administrator protection, bot detection, WooCommerce protection | forced on |
| XML-RPC | blocked, authentication not allowed |
| Logging, "Failed logins", "Requests refused by a block", "Bot detections", "XML-RPC requests", email notifications | forced on |
| Trust the address administrators sign in from | paused |
| Automatic release of blocks | paused: 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 the last valid address in it: a client can prepend fake entries, but the entry your proxy appended sits at the end.
- When the header is missing or holds no valid address, the connection address is used.
- "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-IPorX-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), returntruefromwpus_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,amazonorgoogleusercontent(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
ForwardedandX-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 asCF-Connecting-IP,True-Client-IPandX-Real-IPare suggested beforeX-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
authenticateandwp_authenticate_userwith 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), Blocked IPs and accounts, with their history (CSV), Allowlist (CSV) and Settings (JSON). File names are
wpus-<what>-<date>.csvor.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_capchanges 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_atandsettings(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, 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_hoursandblock_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 fireslogin_form, such as a fully custom AJAX form orwp_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.
6. Admin screens
The plugin adds one top-level menu, User Security (menu position 81, a shield icon), with seven screens. Every screen needs the capability wpus_manage_security (administrators have it) and shares one header: the title, a row of chips ("Version 1.0.0", plus "Emergency mode", "Two-factor on", "Country rule: block FR" or "only" and "Auto-release 24h" when those are active) and a tab bar with live counts on the two block lists. The browser page title of the screens reads "WP User Security", the plugin's earlier name.
| Screen | Path | What it is for |
|---|---|---|
| Dashboard | User Security → Dashboard | Status tiles, a 14-day chart, the latest events and administrator activity. |
| Security Logs | User Security → Security Logs | Every recorded event, filterable. |
| Blocked IPs | User Security → Blocked IPs | IP blocks, with a manual block form. |
| Blocked Users | User Security → Blocked Users | Account blocks, with a manual block form. |
| Allowlist | User Security → Allowlist | Trusted addresses and ranges. |
| Settings | User Security → Settings | Every setting, in cards. |
| Tools | User Security → Tools | Emergency mode, retention, diagnostics, digest, export and import, system information, remembered browsers. |
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:
| Tile | Counts | Opens |
|---|---|---|
| Blocked IPs | IP blocks that are active right now (not limited by the period). | Blocked IPs |
| Blocked users | Accounts 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. | 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:
- 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."
- 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.
Security Logs
Titled "Security Logs". Filters: a search box ("Search reason or agent"; it searches the reason, username, email, IP address and user agent), IP address, 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, with the columns When (with the endpoint underneath), Event (with the user agent and an expandable "detail" line holding the user ID, request method and stored context), Account (username or email, an "admin" pill, and the email), IP address, Reason, 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.
Blocked IPs and Blocked Users
The two screens share one layout. A notice says whether automatic release is on, and the subtitle changes with it ("A block stays until you remove it here — nothing lifts it automatically." or "Automatic blocks lift by themselves after a while; blocks you make by hand and permanent blocks stay until you remove them."). 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>"), Status, Reason, Attempts, Usernames tried, Last detected, Blocked.
- Blocked Users columns: Account (username, with the email underneath), Status, Reason, User ID ("no matching user" when none), Distinct IPs, Last detected, Blocked. Account blocks and user-ID blocks both appear.
- 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.
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.
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), XML-RPC, Blocking behaviour, Country rule, Logging, Notifications, 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.
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:
- 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 blocksand, 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.
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 (73 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 and is blocked after 10 within 60 minutes." (the blocking part also needs the score to reach the block score).
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Login protection ("Score failed logins and apply blocks") | login_protection_enabled | on | Master switch for scoring, recording failures, blocking at sign-in, early refusal and the generic error messages. |
| Suspicious attempts per IP | suspicious_failed_per_ip | 5 | Earlier failures from one address 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 IP | max_failed_per_ip | 10 | Earlier failures from one address that add 60 points. Saved 1 to 1,000. An address is blocked when the score reaches the block score; see the note under the risk signals. |
| Block after failed attempts on one username | max_failed_per_username | 20 | Failures against one username that ask for an account block. Saved 1 to 1,000. |
| Block after failed attempts on one email | max_failed_per_email | 20 | The 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 IPs | unique_ip_threshold | 10 | Distinct addresses with failures against one account that mark a distributed attack (70 points, asks for an account block). Saved 1 to 1,000. |
| Account blocking ("Block accounts that cross a threshold") | account_block_enabled | on | Lets 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_minutes | 60 | How 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_score | 25 | A total at or above this is "suspicious". Saved 1 to 100. |
| Risk score for "block" | risk_block_score | 80 | A 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) | Key | Default | What it does |
|---|---|---|---|
| Administrator protection ("Apply the stricter limits and dedicated logging to administrator accounts") | admin_protection_enabled | on | Applies 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 targeted | admin_max_failed | 5 | The 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 accounts | admin_account_block | off | When 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 logins | log_admin_success | on | Writes "Administrator login" events. |
| Log failed admin logins | log_admin_failure | on | Writes "Administrator login failure" events. |
| Trust the address administrators sign in from | trust_admin_ips | off | Allowlists the address of a completed administrator sign-in for a limited time. See Trusted administrator addresses. |
| Trusted for (days) | trust_admin_ip_days | 30 | How long an automatic entry lasts; each sign-in renews it. Saved 1 to 365. |
| Addresses kept per administrator | trust_admin_ip_limit | 3 | Automatic entries kept per administrator; the oldest is removed first. Saved 1 to 20. |
Bot detection, WooCommerce, default usernames, exploit probing
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Bot detection ("Look for automation signals ...") | bot_detection_enabled | on | Scores automation signals (capped at 35). See Bot detection. |
| Requests per minute that count as high frequency | bot_frequency_per_minute | 10 | The 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_protection | on | Recognises 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_enabled | on | Scores and eventually blocks default usernames that are not accounts. |
| Block the address after this many attempts | generic_username_threshold | 3 | Attempts per address inside the counting window. Saved 1 to 100. |
| Exploit probing ("Block addresses that keep requesting exploit paths") | probe_protection_enabled | on | Counts 404 requests for exploit paths. See Exploit probing. |
| Block after this many probes | probe_threshold | 5 | Probes per address inside the probing window. Saved 2 to 1,000. |
| Counting window (minutes) (in the exploit-probing card) | probe_window_minutes | 10 | The probing window. Saved 1 to 1,440. |
XML-RPC
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Block XML-RPC ("Refuse all XML-RPC requests with a generic 403") | xmlrpc_block | on | Refuses every XML-RPC request. Turn it off only if a plugin or app genuinely needs XML-RPC. |
| Allow XML-RPC authentication | xmlrpc_allow_auth | off | Allows 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 IP | xmlrpc_max_failed | 5 | Refused 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_log | on | Logs allowed XML-RPC calls by method name only. |
Blocking behaviour
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Manual blocking ("Show block and unblock controls on the admin screens") | manual_blocking_enabled | on | When off, creating a block by hand (the two forms and wp wpus block) is refused. Unblocking is unaffected. |
| Allow permanent blocks: IP blocks | permanent_ip_blocking | on | Lets an IP block be permanent (the manual form checkbox, "Make permanent", --permanent). |
| Allow permanent blocks: Account blocks | permanent_user_blocking | on | Lets an account block be made permanent. |
| Release automatic blocks after (hours) | block_auto_release_hours | 0 | 0 means never. Otherwise automatic, non-permanent blocks lift after this many hours. Saved 0 to 8,760. |
| Longer every time (times) | block_escalation_factor | 1 | Each 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_enabled | on | Halves the per-IP limits of an address that was blocked automatically and released by the clock. |
| Remember it for (days) | repeat_offender_days | 30 | How 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_enabled | off | Early refusal of blocked addresses. See Blocks. |
Country rule
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Rule | country_mode | off | off (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 from | country_header | CF-IPCountry | One 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. |
| Countries | country_codes | empty | Comma-separated, upper-case, sorted ISO 3166-1 alpha-2 codes. Unknown codes are dropped on save. |
Logging
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Logging ("Record security events") | logging_enabled | on | Master switch for the security log. |
| Events to record: Successful logins | log_successful_logins | on | "Successful login" events. |
| Events to record: Failed logins | log_failed_logins | on | "Failed login" events. |
| Events to record: Requests refused by a block | log_blocked_requests | on | "Blocked login", block, "Suspicious authentication activity" and "Block record deleted" events. |
| Events to record: Bot detections | log_bot_detection | on | "Bot / automation detected" events. |
| Events to record: XML-RPC requests | log_xmlrpc | on | "XML-RPC request" and "XML-RPC blocked" events. |
| Keep log rows for | log_retention_days | 90 | One 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_days | 30 | How long the counter rows are kept. Saved 1 to 3,650, never below the counting window rounded up to whole days. |
Notifications
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Email notifications ("Send email about security events") | notify_enabled | on | Master switch for alerts and the digest. |
| Send notifications to | notify_email | empty | One valid email address. Empty, or invalid, means the site administration address (Settings → General). |
| Security digest | digest_frequency | weekly | off (Never), daily (Every day), weekly (Every week) or monthly (Every month). Anything else is saved as weekly. |
| Accounts to watch | notify_roles | administrator,editor,author,shop_manager | Role slugs behind the "watched account" alerts. Unknown roles are dropped and administrator is always kept. |
| Notify me about: A new IP address was blocked | notify_ip_blocked | on | See Alerts. |
| Notify me about: An account was attacked from several IP addresses | notify_account_targeted | on | Account blocks and distributed attacks. |
| Notify me about: Somebody tries to sign in to a watched account (wrong password) | notify_admin_failed_logins | on | Also controls the "Administrator block refused" alert. |
| Notify me about: Somebody has the password of a watched account but fails the sign-in code (or the code cannot be emailed) | notify_two_factor_failed | on | Needs the email code to be on. |
| Notify me about: A watched account signs in (every time; off by default) | notify_privileged_signin | off | One email per account and address per repeat window. |
| Notify me about: A burst of traffic on the login page | notify_attack_spike | on | See Bot detection. |
| Notify me about: XML-RPC is being hammered | notify_xmlrpc_attack | on | Refused XML-RPC requests. |
| Repeat window (minutes) | notify_throttle_minutes | 15 | At 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) | Key | Default | What it does |
|---|---|---|---|
| Trust proxy headers ("Read the client address from a proxy header instead of REMOTE_ADDR") | trust_proxy_headers | off | Use the address in the chosen header. Only turn it on when infrastructure you control sets the header. |
| Proxy header | proxy_header | X-Forwarded-For | One 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) | Key | Default | What it does |
|---|---|---|---|
| Ask for a sign-in code | two_factor_enabled | off | Master switch. Off by default because it can refuse a correct password when mail fails. |
| Who is asked | two_factor_roles | administrator,editor,author,shop_manager | Role 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_forms | on | The 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_minutes | 10 | How long a code stays usable. Saved 1 to 60. |
| Wrong codes per email | two_factor_max_attempts | 5 | Wrong codes before the code is burnt and a new one is sent. Saved 1 to 10. |
| Remember a browser for (days) | two_factor_device_days | 30 | How long a browser that gave a correct code is not asked again. Saved 1 to 365. |
Privacy and emergency mode
| Setting (label) | Key | Default | What it does |
|---|---|---|---|
| Sign-in notice ("Show a short privacy notice under the sign-in forms") | login_notice_enabled | off | Prints the notice on the sign-in forms. See Privacy tools. |
| Notice text | login_notice_text | empty | Your 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_uninstall | on | When 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_mode | off | Switched only on User Security → Tools. A save from the Settings form never changes it. |
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.
| Command | What 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. --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 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 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 retention | Runs the retention cleanup now and reports the rows removed. Blocks are not touched. |
wp wpus release | Releases 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 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
| Action | Parameters | Fires when |
|---|---|---|
wpus_ip_blocked | $ip, $block_id, $reason, $manual (bool) | A block row for an address is written, 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), $reason | A new address block starts (or a lifted one starts again). The alert listens to this. |
wpus_account_block_created | $row, $account (resolved account array), $reason | A 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, $reason | A block of an account was asked for but refused (administrator protection, or account blocking is off). |
wpus_block_unblocked | $row (as it was before), $reason | A block row is lifted by a person, WP-CLI or the automatic release. Fires once per row. |
wpus_login_blocked | $action (type, value, reason), $account | A 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, $account | A wrong password for an administrator account. It no longer sends email itself. |
wpus_privileged_login_failed | $result, $account | A wrong password for an account in a watched role, administrators included. Drives the alert. |
wpus_suspicious_activity | $result, $account | A suspicious decision was logged (at most one per address and identifier every 10 minutes). |
wpus_bot_detected | $detection (signals, score, reason, is_bot), $account | A failed attempt carried bot signals. |
wpus_attack_spike | $frequency (per_minute, per_hour), $ip | An address reached the high-frequency rate on a sign-in endpoint (at most once per address every 5 minutes). |
wpus_xmlrpc_blocked | $ip | A request was refused while XML-RPC is blocked. |
wpus_allowlist_added | $entry, $id | An allowlist entry was added (by hand, WP-CLI or the trusted-address feature). |
wpus_allowlist_removed | $entry, $id | An allowlist entry was removed. |
wpus_two_factor_failed | $user (WP_User), $why | A 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. |
<?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
| Filter | Parameters | What it changes |
|---|---|---|
wpus_settings | $settings | The effective settings for the request, after emergency mode. Force values from code. The Settings screen still shows the saved values. |
wpus_settings_sanitized | $clean, $input | The settings array right before it is stored. |
wpus_risk_signals | $result, $context, $account | The 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, $context | The bot signals before they are scored and capped. Each signal is an array with code, score and reason. |
wpus_log_data | $row, $event | A log row right before it is written. |
wpus_manage_capability | $cap | The capability that gates every screen and action (default wpus_manage_security). The WPUS_MANAGE_CAP constant takes precedence. |
wpus_generic_login_error | $message | The one message every credential failure returns. |
wpus_erase_attempts | $allowed (default true) | Whether a privacy erasure also clears attempt rows. |
wpus_attributable_ip | $ok, $ip | Whether the automatic rules that block or trust a single client may act on this address. |
wpus_trust_admin_ip | $ok, $ip, $user | Whether an address may be allowlisted after an administrator signed in. |
wpus_generic_usernames | $names | The default-username list (lower-case). |
wpus_probe_rules | $rules | The 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, $user | Whether 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, $data | The threat level (low, medium, high; anything else is ignored). |
wpus_digest_lines | $lines, $data | The lines of the digest email. |
wpus_export_row_cap | $cap (default 100000) | The most rows one exported CSV holds. |
<?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.
| Hook | Priority | What the plugin does |
|---|---|---|
authenticate (filter) | 1 | Returns 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. |
authenticate | 99 | The email sign-in code check: returns an error until a valid code is entered. |
authenticate | 100 | The 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, 30 | 5: 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 10 | 0: early refusal of a blocked address (when enabled). 10: counts the request on the login page. |
init (action) | 1 and 10 | 1: 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) | 100 | Replaces credential error messages with the generic message. |
rest_authentication_errors (filter) | 100 | Replaces a credential error with one generic 401. |
application_password_is_api_request (filter) | 10 | Returns false for a blocked address or account so WordPress skips application-password authentication. |
application_password_failed_authentication, application_password_did_authenticate (actions) | 10 | Records a failed application-password attempt; logs a successful one. |
xmlrpc_enabled (filter), xmlrpc_call (action), wp_die_xmlrpc_handler (filter) | 10 | Switches 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) | 10 | Withholds 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) | 10 | Print 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) | 10 | Print the code box on the WooCommerce form; print the sign-in notice at the end of the form. |
wp_loaded (action) | 5 and 10 | 5: observes a WooCommerce sign-in submission. 10: runs the schema check and re-schedules the hourly release event. |
template_redirect (action) | 0 | Looks at a 404 for an exploit path. |
user_register, profile_update, after_password_reset, deleted_user, set_user_role (actions) | 10 | Remember 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. |
plugins_loaded, wp_initialize_site, cron_schedules | 5, 20, 10 | Boot 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_scripts | 10 | Menus, 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) | 10 | The 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) | 10 | Register 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_dois the single endpoint for every change made on the plugin screens. It requires the managing capability and a valid nonce (nonce actionwpus_action, field_wpnonce), runs the action named bywpus_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_digestandimport_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).dayslimits the log only.- Settings are saved through
options.phpwith the option groupwpus_settings_groupand the optionwpus_settings. A filter onoption_page_capability_wpus_settings_groupmakes WordPress require the plugin's capability instead ofmanage_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). Logswpus_date_from,wpus_date_to(YYYY-MM-DD),wpus_ip,wpus_username,wpus_email,wpus_event,wpus_risk,wpus_result,wpus_admin,s. Block listswpus_status(active,blocked,permanent,unblockedor empty for any),wpus_period,s. Formswpus_row[](row IDs),wpus_bulkandwpus_bulk2(the top and bottom bulk-action selects),wpus_ip,wpus_identifier,wpus_reason,wpus_permanent,wpus_entry,wpus_label,wpus_state(onoroff, for emergency mode),wpus_days,wpus_device,wpus_settings_jsonandwpus_filter(the filter button). The sign-in code is the POST fieldwpus_code; the redirect after a voided sign-in addswpus_2fa=requiredto the login URL.
Capability, constant and roles
- Capability
wpus_manage_securitygates every screen, button, download and the settings save. It is added to theadministratorrole 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 inwp-config.phpto require another capability, for exampledefine( 'WPUS_MANAGE_CAP', 'manage_options' );. It takes precedence over the filterwpus_manage_capability. - Constants the plugin defines (each only if not already defined):
WPUS_VERSION(1.0.0),WPUS_DB_VERSION(1.2.0),WPUS_FILE,WPUS_DIR,WPUS_URL,WPUS_SLUG(wp-user-security-guard) andWPUS_TEXTDOMAIN. - The function
wpus_guard()returns the main plugin object.
Scheduled events
| Event | Schedule | What runs |
|---|---|---|
wpus_daily_maintenance | Daily (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_blocks | Hourly | Releases automatic blocks whose expiry has passed. It only looks at blocks that carry an expiry, so with automatic release off it finds nothing unless such blocks were created earlier; 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.
{prefix}wpus_blocks
Columns: id, block_type VARCHAR(16) (ip, account or user), value VARCHAR(191) (the address, 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 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
| Kind | Name | Holds |
|---|---|---|
| Option | wpus_settings | All settings (autoloaded, one read per request). |
| Option | wpus_db_version | The installed schema version (autoloaded). |
| Option | wpus_digest_last | The 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_status | The licence key and its status (active or inactive). Not autoloaded. Not removed by this plugin's uninstall. |
| User meta | wpus_2fa_pending | A hash of the outstanding sign-in code. |
| User meta | wpus_2fa_sent | When that code was emailed (Unix time). |
| User meta | wpus_2fa_tries | Wrong codes entered for the current code. |
| Transient | wpus_allowlist_cache | The allowlist (10 minutes). |
| Transient | wpus_notice_<user id> | The result notice after an action (5 minutes). |
| Transient | wpus_notify_<hash> | The alert throttle per type and subject (the repeat window). |
| Transients | wpus_rl_60_<hash>, wpus_rl_3600_<hash> | Request counters per address for a minute and an hour. |
| Transient | wpus_gu_<hash> | Default-username counter per address (the counting window). |
| Transient | wpus_pr_<hash> | Exploit-probe counter per address (the probing window). |
| Transients | wpus_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). |
| Site transient (licence client) | wpxh_licence_check_v2 | The 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. |
| Cookie | wpus_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, block lifecycle, IP blocker, user protection, bot detection, generic usernames, probe protection, 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).admin/views/:dashboard.php,logs.php,blocks.php,allowlist.php,settings.php,tools.php.assets/css/admin.cssandassets/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.licence/: the bundled licence and update client.languages/wp-user-security-guard.pot: the translation template. No translations are shipped, andload_plugin_textdomain()is called only when the plugin is not in the folderwp-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.
| Topic | Detail |
|---|---|
| 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). Blocks table: the blocked address, 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. Options: your settings, the schema version, the digest clock, and the licence key and status. |
| What is never stored | Passwords, 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. Exception: 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, so a password with an @ typed into that box would be stored. |
| Where it lives | In the WordPress database of the site (five tables with your table prefix and the options above). Time stamps use the site's timezone. |
| Retention | Log 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. |
| Cookies | One, 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.com | Only 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.There is no tracking, no analytics, no GeoIP lookup and no blocklist feed. |
| Downloads | The 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 text | The plugin offers suggested text under Settings → Privacy → Policy Guide ("Security log"). It says what is recorded, that passwords and tokens are not, how long the log is kept, and that a block is not removed by an erasure request. |
| Exporter and eraser | "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 notice | Optional. Tells visitors that their IP address and browser details are recorded, with a link to your privacy policy. See Privacy tools. |
| Deactivation | Removes 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 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
| Problem | Likely 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. It does not lift a block that already exists, 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 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. Turn on proxy trust (behind a proxy), or return true from wpus_attributable_ip on a private network you know is not shared. |
| An attacker tries many passwords or usernames and nothing is blocked. | The per-IP limit adds 60 points and the block score is 80, so a script that looks like a normal browser and uses unknown usernames is marked suspicious, not blocked. See How a sign-in attempt is judged. Lower Risk score for "block", or rely on the other signals (real accounts, default usernames, bot signals). |
| 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 Email notifications, 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 the setting must be above 0. 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. |
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.
Why did a block need one more failure than my limit?
The counters are read before the failure that just happened is saved. With a limit of 10, ten earlier failures must exist, so the block lands on the 11th failed attempt.
Why is a script guessing many passwords only marked suspicious?
Reaching the per-IP limit adds 60 points and the block score is 80. A block needs a second signal, such as failures against a real account, an automation user agent, missing browser headers or a default username. Unknown usernames are not stored by name, so they add nothing on the account side. See How a sign-in attempt is judged.
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. 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.
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.
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 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.
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. 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.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,exportandimport-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.0.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 diagnosehelp 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.