Documentation
M Security
M Security is a login and spam defence plugin for WordPress: it counts failed sign-ins per IP address and per username, locks out repeat offenders, can refuse logins by country, and guards comments, registration and front-end forms. Everything runs on your own server by default, and the optional shared threat feed stays off until you switch it on. This manual is for the administrator who has just installed it and has to configure it correctly the first time, including the settings that can lock you out of your own site and the ones that quietly turn away real visitors.
Installing and activating
M Security requires WordPress 6.5 or newer and PHP 8.1 or newer. There is nothing to prepare before activation: the plugin ships with working defaults and starts protecting wp-login.php the moment it is switched on, including on sites running a maintenance-mode plugin.
- Upload the plugin to /wp-content/plugins/m-security/, or install the zip from Plugins, Add New, Upload Plugin.
- Activate it. Three database tables are created (wp_msc_log, wp_msc_lockouts, wp_msc_cloud), the default settings are written, and a daily maintenance job is scheduled.
- The address the activation request arrived from is written into the Allowlist automatically.
- Open M Security in the admin menu. The Statistics tab opens first and will be empty until traffic arrives.
Behind Cloudflare or any other proxy the address seeded into the Allowlist on activation is the proxy's, not yours: there is nowhere to configure the trusted header before the plugin exists, so it can only see the connecting address. Nothing ever removes that line. While it is there, every visitor arriving through that edge is treated as trusted and is exempt from login limiting, country rules, the Denylist, permanent bans, the comment and registration guards and the shared feed. The site looks protected and protects nothing. Configure the proxy settings first, then press "Add my IP to the allowlist" in the sidebar, then open Access lists and delete the seeded proxy line by hand.
What is already running the moment you activate
Most protections are on by default; the ones that can ban an address permanently are switched off. Several defaults change how the site behaves for real visitors and integrations, so read the list before you leave the screen.
- Login limiting is on: 4 failed attempts, then a 20 minute lockout, doubling for repeat offenders up to a 24 hour cap.
- Locked-out clients get an empty 403 page instead of the login form, login errors are generic, and password resets are throttled.
- XML-RPC is disabled. Jetpack, the WordPress mobile apps and remote publishing tools stop working until you turn it back on.
- Author enumeration is blocked: a logged-out visitor requesting any URL containing ?author= receives an empty 403. It is the only hardening rule that refuses ordinary front-end page requests; the comment, registration and form guards act on their own submission paths as well.
- The REST user listing is hidden from anonymous visitors, the WordPress version is hidden, oEmbed author data is stripped, and users are removed from the sitemap.
- Comment and registration guards are on: invisible honeypot, a 4 second minimum before submit, a JavaScript check, the disposable-domain list and an MX record check.
- Front-end form protection is on: a small script appends an invisible honeypot to every POST form on the page, and Contact Form 7, WPForms and Gravity Forms submissions are scored as spam when a guard fires.
- Instant-ban usernames is empty, so nothing is ever banned permanently until you fill that field.
- Country rules do nothing: Allowed countries is empty and no GeoIP database is installed.
- The shared threat feed is off. Nothing is downloaded, nothing is sent.
- Application passwords remain enabled.
- Every event is logged and kept for 30 days, with IP addresses stored in full.
Where the screens are
M Security adds one top-level menu item with a shield icon, near the bottom of the wp-admin menu, at admin.php?page=m-security. Every screen and every action requires the manage_options capability, so only administrators see it. The interface is in English on most sites, so the labels below are given exactly as they appear.
- Statistics: what was blocked over the last 7 or 30 days, a chart drawn as inline SVG, the busiest source countries, the shared feed's contribution, and the weekly e-mail settings.
- Login protection: retry counts, lockout durations and the instant-ban username list.
- Country rules: the allowed country list, which gateways it applies to, the GeoIP database and the proxy settings.
- Spam: comment, registration and form guards.
- Hardening: XML-RPC, enumeration blocking, application passwords and the two optional ban triggers.
- Access lists: the Allowlist, the Denylist and the "Permanently banned IPs" panel.
- Cloud: the optional shared threat feed and its status card.
- Logs & privacy: the event log with filters, CSV export, IP storage mode and retention.
- The sidebar "Status & tools" panel is on every tab: counts for the last 24 hours, the number of permanently banned addresses, your own IP and country, the GeoIP status, an "Add my IP to the allowlist" button and, in the card below it, "Active lockouts" with a Release link on each row.
- The WordPress Dashboard also gets an M Security widget showing the same 7 and 30 day chart of blocked attacks.
Login protection settings and their defaults
This tab controls the brute-force counters. Failures are counted per IP address and per username across wp-login.php, XML-RPC and application passwords, and a lockout is applied every time the counter reaches a multiple of the retry limit. Allowlisted addresses are not counted and are not locked. That exemption covers the counters; it does not cover the decoy-username list, which is explained in section 15.
- Limit login attempts: enabled.
- Allowed retries: 4, range 1 to 20.
- Lockout duration (minutes): 20, range 1 to 1440.
- Escalate repeat lockouts: enabled. Each further lockout doubles the duration.
- Maximum lockout (hours): 24, range 1 to 168.
- Reset counters after (hours): 12, range 1 to 168.
- Lock by username too: enabled. A targeted username is locked from every address, which also means an attacker rotating addresses can lock a real account.
- Serve locked-out bots a blank page: enabled.
- Generic login errors: enabled, so the form never reveals whether the username or the password was wrong.
- Throttle password resets: enabled. Every reset request is counted, not only failed ones, and it goes onto the same per-IP counter the login page reads. At the default retry limit the fourth request produces a 20 minute lockout on the login page as well.
- Log successful logins: enabled.
- Notification email: empty, which means the site admin address is used.
- Email after N lockouts: 0, which disables the notification. Range 0 to 100.
- Ban IPs that use those usernames: enabled, but inert while the list below is empty. With it off the attempt is still refused with a blank 403 and is logged, it simply does not persist a ban.
- Instant-ban usernames: empty. This is the only ban trigger that is armed out of the box once you fill it in.
Country rules and the GeoIP database
Country rules do nothing until two conditions are met: the Allowed countries field holds at least one ISO 3166-1 alpha-2 code, and a valid MaxMind format .mmdb database is installed. The login page always fails open, so a visitor whose country cannot be determined is never blocked there, and private or reserved addresses are exempt everywhere.
- Allowed countries: empty. Comma separated codes, for example LT, DE, US. Emptying this field switches all country rules off.
- Apply to login page: enabled. Apply to registration: enabled. Apply to comments: enabled. Apply to third-party forms: disabled.
- Strict mode for registration/comments: disabled. When enabled, an undeterminable country blocks registration and comments, and it blocks Contact Form 7, WPForms and Gravity Forms submissions too whenever "Apply to third-party forms" is on. The label does not mention the forms. The login page still fails open.
- GeoIP database source: "Manual upload (.mmdb file)" by default. "MaxMind GeoLite2 (account required, auto-updates twice weekly)" needs an account ID and licence key; "DB-IP Country Lite (no account, auto-updates monthly)" needs no account.
- MaxMind account ID and MaxMind license key: empty.
- Client IP header: None, meaning the connecting address is used. The other options are CF-Connecting-IP, X-Forwarded-For, X-Real-IP and True-Client-IP.
- Trusted proxy addresses: empty. The chosen header is honoured only for requests that actually arrive from these addresses, so it cannot be spoofed.
- The "GeoIP database" card below the form offers "Upload .mmdb" and "Download / refresh now", and the screen warns when the active database is more than 35 days old. "Download / refresh now" only does something when the source is MaxMind or DB-IP; see section 19.
The country allowlist is enforced on wp-login.php, on XML-RPC and on application-password logins. If you set countries and then travel, use a VPN or roam on a foreign mobile network, a fresh sign-in from that country is refused with a blank 403. The exemption for a request carrying a valid login cookie exists on the wp-login.php path only. XML-RPC and application-password logins are country-checked with no cookie exemption at all, and the Denylist and permanent-ban checks on wp-login.php have no cookie exemption either, so an open wp-admin session does not keep a banned or denylisted address on the login page.
Spam defence
The Spam tab uses invisible checks only: no CAPTCHA and no third-party service. What a honeypot hit costs depends on where it happens. A comment is refused outright with an empty 403. A registration is refused with the message "Registration could not be completed." A Contact Form 7 or Gravity Forms submission is only flagged as spam, and what happens next belongs to those plugins: Contact Form 7 answers the sender with its own generic error message, Gravity Forms accepts the submission and files the entry under Spam, and WPForms shows "Your submission could not be processed." The timing and JavaScript checks are soft signals that route a comment into the spam queue instead. None of these checks apply to comment moderators or to allowlisted addresses.
- Comment honeypot: enabled. A bot that fills the invisible field is blocked with a blank page.
- Minimum seconds before submit: 4, range 0 to 60. Faster comments go to the spam queue. 0 disables the check.
- Require JavaScript for comments: enabled. Comments from clients without JavaScript go to the spam queue.
- Protect front-end forms: enabled. A script adds an invisible honeypot to every method=post form on the page, not only to the ones the plugin owns.
- Form plugin integrations: enabled. Contact Form 7, WPForms and Gravity Forms submissions are scored as spam when a guard fires.
- Registration honeypot & timing: enabled. The registration form uses a fixed 4 second minimum, independent of the comment setting; unticking this checkbox is the only way to switch it off.
- The hidden timestamp that carries the timing check is signed and is accepted only between the minimum age and 4 weeks. Past 4 weeks it is rejected, which matters on cached pages; see section 17.
- Block disposable email domains: enabled, against a bundled list of throwaway providers.
- Verify email domain (MX record): enabled. Results are cached for 24 hours, and the check is skipped when the server cannot do DNS lookups at all.
- First-comment moderation: this checkbox writes the WordPress core option comment_previously_approved, not a plugin setting, so it survives uninstalling the plugin.
Hardening
These are independent switches, each of which can be turned off on its own. Six of them are on by default; five change nothing a normal visitor sees, and the sixth, the author enumeration block, refuses front-end URLs. The two ban triggers at the bottom are off, and they are the ones to think about twice.
- Disable XML-RPC: enabled. xmlrpc.php answers with an empty 403 before the XML-RPC server boots, and the X-Pingback header is removed. Turn this off if you use Jetpack or the WordPress mobile apps.
- Hide REST user listing: enabled, for visitors who are not signed in.
- Block author enumeration scans: enabled. Any front-end request with ?author= from a signed-out visitor gets an empty 403, on any URL. Being on the Allowlist does not prevent that refusal.
- Hide WordPress version: enabled. Strip author from oEmbed: enabled. Remove users from sitemaps: enabled.
- Disable application passwords: disabled. Turning it on removes the application-password login path completely, which breaks REST integrations that rely on it.
- Ban IPs that probe XML-RPC: disabled. When on, any request to xmlrpc.php while XML-RPC is disabled results in a permanent ban of that address.
- Ban IPs that scan for usernames: disabled. When on, any signed-out request containing ?author= results in a permanent ban. It needs "Block author enumeration scans" to stay on.
- File editor: a read-only status row. It reports whether DISALLOW_FILE_EDIT is defined in wp-config.php and suggests adding it.
The two ban triggers create permanent bans by IP address with no expiry. They do not distinguish an attacker from a search engine crawler, an uptime monitor, a backup service or a plugin on another site of yours that still speaks XML-RPC. If any page on the site links to a URL containing ?author=, the crawler that follows it will be banned.
Access lists: allowlist, denylist, permanent bans
Three different things live on this tab, and confusing them is the most common source of support questions. The Allowlist overrides almost everything else in the plugin, with two exceptions named below. The Denylist holds manual entries only. Automatic bans are stored separately, in their own panel.
- Allowlist: empty apart from the address seeded at activation. One IP or CIDR range per line. These addresses are never rate limited, never geo-filtered, never banned and never reported to the shared feed.
- The two exceptions: an allowlisted address that submits a name from the Instant-ban usernames list, or that requests a front-end URL containing ?author= while signed out, still receives a blank 403. The allowlist prevents the ban, not the refusal. Section 15 explains both.
- Denylist: empty. One IP or CIDR range per line, manual entries only. These addresses get a blank 403 on the login page, cannot register or comment, and their Contact Form 7, WPForms and Gravity Forms submissions are scored as spam. Adding an entry also queues it for the shared blacklist when reporting is switched on.
- Permanently banned IPs: the panel under the form, listing addresses banned automatically by the decoy username, XML-RPC probe or username scan triggers. Each row has an "Unban" link and there is a "Clear all bans" button. The 100 most recent are shown, up to 50000 bans are kept, and the oldest are evicted first.
- Automatic bans never appear in the Denylist textarea. The sidebar shows the ban count and links here.
Statistics, the dashboard widget and the weekly e-mail
The Statistics tab opens first and shows what the plugin actually stopped over the last 7 or 30 days: attacks blocked, sign-ins blocked, spam blocked, failed passwords and distinct addresses, with a chart drawn as inline SVG that needs no JavaScript and no external request. When IP storage is set to truncated, the last figure counts networks rather than addresses and the label changes accordingly. Below the chart are the source countries and, when the shared feed is on, how much of the work it did.
- Send a weekly summary: disabled. When switched on it goes out every Monday at 08:00 site time and compares the week against the one before it.
- Send to: empty, which means the site administrator address shown in the field placeholder.
- "Send a test report now" sends immediately, to the address above, whether or not the weekly schedule is enabled. It is the fastest way to prove that this site can send e-mail at all.
- The WordPress Dashboard widget carries the same 7 and 30 day chart and is visible only to users with manage_options.
Logs and privacy settings
Every login attempt, lockout, block and spam verdict is written to the log table with a timestamp, an address, a country, a gateway, a result and a rule code. The Logs & privacy tab filters and searches that table, exports it as CSV and clears it. A daily job removes entries older than the retention period, and a hard cap of 100000 rows is enforced so a flood cannot fill the database.
- IP address storage: Full (default), Truncated or Hashed. Truncated keeps the /24 for IPv4 and the /64 for IPv6; hashed stores a salted digest. In the two non-full modes the one-click allow and deny links on log rows cannot work.
- Keep log entries (days): 30, range 1 to 365.
- Keep data on uninstall: disabled.
- "Export CSV" streams up to 100000 rows; values beginning with a formula character are quoted so a spreadsheet cannot execute them.
- "Clear log" empties the table and therefore the statistics screen. It cannot be undone.
- The daily purge never touches permanent bans. They carry an expiry of 2099-12-31 23:59:59 and are removed only by Unban, by "Clear all bans" or by hand in the database. The log rows that explain them, however, are deleted on the ordinary retention schedule.
A realistic first run, in order
Configure the plugin in this order. Each step assumes the previous one is done, and the order deliberately puts everything that can lock you out after the step that makes you immune to it.
- If the site is behind Cloudflare, a load balancer or any other proxy, open Country rules first, set Client IP header and list the proxy ranges under Trusted proxy addresses. Save.
- Look at the sidebar. The address under "Your IP" must be your real address. Press "Add my IP to the allowlist" if the button is there.
- Open Access lists and read the Allowlist line by line. Delete the entry that was seeded at activation if it is the proxy's address rather than yours. When you are done the list should contain your real address and nothing that looks like a CDN or load-balancer edge.
- Open Hardening. Decide about XML-RPC: leave it disabled unless you use Jetpack or the mobile apps. Leave both ban triggers off for now.
- Open Login protection. Adjust the retry count and lockout duration if you want, and leave Instant-ban usernames empty for the moment.
- In wp-admin open Users and write down every real account name. Only then return to Login protection and add decoy names that appear nowhere on that list.
- Set up country rules only if you need them: install a GeoIP database first, confirm the sidebar shows your own country correctly, and only then fill in Allowed countries.
- Open Logs & privacy and choose an IP storage mode and a retention period that match your privacy policy.
- Optionally enable the weekly report on the Statistics tab and press "Send a test report now" to prove that e-mail works.
- Leave the Cloud tab for last, and only if you want the shared feed. Enable it, press "Test connection", then "Sync now", and check the status card.
- Come back to the log after a week. Only then decide whether the XML-RPC and username scan ban triggers make sense for this site.
The emergency switch: MSC_DISABLE
One line in wp-config.php switches the plugin off completely. The constant is read at the very top of the main plugin file, before anything else loads, so the firewall, every ban, every lockout, every country rule and the admin screens all stop existing for as long as the line is there. Nothing is deleted: settings, bans and logs stay exactly as they were, and removing the line brings them all back. This is the recovery route that works even when you cannot reach wp-login.php at all.
- Open wp-config.php over FTP, SFTP or the hosting file manager.
- Add the line below anywhere above the comment that says that is all, stop editing.
- Reload wp-login.php and sign in normally.
- Fix whatever locked you out: unban the address, release the lockout, clear the decoy username list or empty Allowed countries.
- Remove the line from wp-config.php and reload the site to confirm protection is back.
define( 'MSC_DISABLE', true );
While the constant is defined the site has no login protection at all, so treat it as a temporary measure and remove it as soon as you are back in. The plugin still appears active on the Plugins screen, but its menu is gone, which is normal.
Every way this plugin can lock you out
Read this section before you change anything on the Login protection, Country rules or Hardening tabs. Two comforting ideas are only partly true, so do not lean on them. The firewall acts on wp-login.php and xmlrpc.php, but the author enumeration block is hooked on init and refuses any signed-out front-end URL containing ?author=, on any address. And the exemption for a valid login cookie exists only on the country check on wp-login.php: the Denylist and permanent-ban checks there have none, so an open wp-admin session does not guarantee that you can reach the login page again. Allowlisting your own address is still the single most useful thing you can do; section 15 sets out what it does not cover.
- A decoy username that is also a real account. The very first sign-in attempt with that name permanently bans the address that used it and returns an empty 403, over wp-login.php, XML-RPC and application passwords alike. Recovery: Access lists, "Permanently banned IPs", Unban, then remove the name from the list.
- Four password reset requests. "Throttle password resets" counts every "Lost your password?" submission, not only failed ones, onto the same per-IP counter the login page reads. At the default retry limit of 4 the fourth request applies a 20 minute lockout, and the login page then answers with a blank 403. One user clicking the button four times, or four colleagues behind one office address, is enough. Recovery: sidebar, "Active lockouts", Release; or wait it out; or MSC_DISABLE.
- The two Hardening ban triggers. With them on, one request to xmlrpc.php, or one signed-out request containing ?author=, is enough for a permanent ban. Recovery: the same panel, plus turning the trigger back off.
- A denylist entry or CIDR range that covers your own address. Recovery: remove the line, or add your address to the Allowlist, which wins on every path that consults it.
- Country rules that do not include where you actually are. Travel, a VPN exit node, a mobile network registered abroad or a stale GeoIP database will all do it. Recovery: empty Allowed countries, or untick "Apply to login page".
- "Lock by username too" lets an attacker with many addresses lock a real account name from everywhere. Recovery: sidebar, "Active lockouts", Release; or turn the setting off.
- A misconfigured proxy setup. If Client IP header is wrong or the proxy ranges are not listed, every visitor resolves to the same address, so one attacker's lockout applies to your whole audience. Recovery: fix the two proxy fields on Country rules.
- "Disable application passwords", and the ban, country and shared-feed checks on the application-password channel, silently break integrations that authenticate that way. They fail with a refusal, not with an explanation.
- The shared threat feed, once enabled, can refuse an address on your login page that you never blocked yourself. Recovery: allowlist the address, or switch "Block addresses from the shared feed" off.
Never put a real account name in Instant-ban usernames. The ban is by IP address, it is permanent, it has no expiry, and it is applied before the password is ever checked, so a colleague who simply mistypes into the wrong site can take your whole office off the login page.
What the Allowlist does not cover
On almost every path the Allowlist is checked first and the request is let through: the wp-login.php firewall, the authenticate filter, the application-password path, the comment guard, the registration guard and the form adapters all do this. Two paths do not, and both of them end in a completely blank page with no error, no lockout notice and, in most cases, no log row to explain it. A third, xmlrpc.php, is refused before the Allowlist is consulted at all, although it is refused for every address alike. This is the single most likely support call this plugin will generate, so read it before you conclude the site is broken.
- The decoy username list. When someone signs in with a name from Instant-ban usernames, the plugin asks for a ban, the ban is declined because the address is allowlisted, and no log row is written; the request is then refused with a blank 403 anyway. So an allowlisted administrator who types a decoy name sees nothing at all: no error, no ban, no log entry. If your login page goes blank and your address is on the Allowlist, you typed a name that is on the decoy list. Sign in with a real account name.
- The author enumeration block. A signed-out request to any front-end URL containing ?author= gets a blank 403 whether or not the address is allowlisted. With "Ban IPs that scan for usernames" on, an allowlisted address is refused and not banned, and again no log row is written. Sign in first, or drop the ?author= parameter from the URL.
- xmlrpc.php, which is checked before the Allowlist and is not an exception to it: while "Disable XML-RPC" is on, that file answers with a blank 403 for everyone, allowlisted or not. What the Allowlist changes is only the record. With "Ban IPs that probe XML-RPC" also on, an allowlisted address is refused and not banned, and no log row is written at all, while the same request with that trigger off is logged. A missing log row is therefore not proof that the request never arrived.
- What the Allowlist does reliably prevent: the login counters, the lockout, the country checks, the Denylist and permanent-ban checks on every path that consults it, the comment and registration guards, the third-party form adapters, being reported to the shared blacklist, and being banned by any of the three ban triggers.
- If you need to be sure an address is exempt from everything, the only complete off switch is MSC_DISABLE in wp-config.php.
How real visitors get turned away without a word
These failures do not lock you out, so nobody reports them; they cost you enquiries, registrations and comments instead. Each one has a specific cause and a specific switch.
- Enquiries vanish silently. With "Protect front-end forms" on, a script appends an invisible text input to every method=post form on the page, including checkout, search, and forms owned by other plugins. Browser autofill and password managers will fill hidden text inputs. With "Form plugin integrations" also on, a filled field marks a Contact Form 7 or Gravity Forms submission as spam: Contact Form 7 answers the sender with its own generic error message, and Gravity Forms accepts the submission, shows the usual confirmation and files the entry under Spam, where nobody looks. Recovery: untick "Protect front-end forms", or untick "Form plugin integrations".
- Registration stops working on a heavily cached site. The signed timestamp in the form is accepted only while it is between the minimum age and 4 weeks old. A registration page held in a full-page or CDN cache for more than 28 days fails for every visitor with "Registration could not be completed.", and a comment form in the same state routes every comment to the spam queue. Recovery: purge the page cache, exclude the registration and comment pages from full-page caching, or set "Minimum seconds before submit" to 0 for comments. The registration form's own 4 second minimum can only be switched off by unticking "Registration honeypot & timing".
- Signed-in members get a blank page when they comment. The comment guard exempts only users who can moderate comments. A logged-in subscriber, customer or member commenting from outside Allowed countries gets an empty 403 mid-session, with a valid login cookie. Recovery: untick "Apply to comments", or allowlist the range.
- One banned shared address kills the contact form too. The permanent-ban check runs on comments, registration and every third-party form submission, not just on the login page. Recovery: Unban the address and allowlist the range, then check the form plugin's own entries or logs for what was lost.
- Comments from a handful of readers always land in spam. "Require JavaScript for comments" does that to any client without JavaScript.
Recovery without wp-admin: database and WP-CLI
When you have shell or database access, you do not need the admin screens at all. Bans and lockouts live in one table, wp_msc_lockouts, where a row with kind = 2 is a permanent ban and any other row with a locked_until value in the future is a temporary lockout. Permanent bans carry the sentinel expiry 2099-12-31 23:59:59, so they look like active lockouts to a naive query. Settings live in a single option, msc_settings. Adjust the wp_ prefix to whatever this installation actually uses. The plugin ships no WP-CLI commands of its own, so the lines below are plain core commands.
- List what is banned before you delete anything, so you know what you are undoing.
- Remove the permanent bans, or deactivate the plugin outright if you cannot edit wp-config.php.
- If you have no file and no shell access at all, renaming wp-content/plugins/m-security through the hosting file manager is the last resort, not an equivalent of deactivating. WordPress drops a plugin whose file has disappeared at the next admin page load, but the deactivation hook never runs, so msc_purge, msc_geo_update, msc_cloud_sync and msc_weekly_report stay scheduled against a plugin that is no longer there. MSC_DISABLE in wp-config.php is the cleaner route because it leaves the plugin's own state consistent.
- Sign in, fix the setting that caused it, and reactivate or restore the folder. If you did rename the folder, save the Country rules and Cloud tabs once each afterwards so the GeoIP and feed jobs are re-aligned; the daily purge, and the weekly report while it is switched on, are re-created on their own at the next admin page load.
wp db query "SELECT subject, kind, locked_until FROM wp_msc_lockouts ORDER BY first_fail DESC LIMIT 50;"
wp db query "DELETE FROM wp_msc_lockouts WHERE kind = 2;"
wp db query "DELETE FROM wp_msc_lockouts WHERE kind != 2 AND locked_until IS NOT NULL;"
wp option patch update msc_settings ip_allowlist "203.0.113.10"
wp plugin deactivate m-security
Take a database backup first. The second command removes every permanent ban on the site. The third releases every temporary lockout and nothing else: the kind != 2 clause is what keeps it away from the permanent bans, which sit in the same table with locked_until set to 2099-12-31 23:59:59, so a plain WHERE locked_until IS NOT NULL would delete every ban as well. Neither can be undone. The fourth replaces the whole allowlist with the single address you give it, so read the current value with wp option get msc_settings before you overwrite it.
Troubleshooting
These are the things that actually go wrong, and what to look at first. Most of them are not faults: they are two different mechanisms being mistaken for one.
- Thousands of blocked requests but nothing banned. Blocking refuses one request; banning persists an address. Only the decoy username list, and optionally the two Hardening triggers, ever create a ban.
- A ban you know exists is not in the Denylist field. It never will be: automatic bans are stored separately and listed under "Permanently banned IPs".
- Country rules appear to do nothing. Check that a GeoIP database is installed, that the sidebar shows a country for your own address, and that Allowed countries is not empty. The login page always fails open when the country is unknown.
- Every log row shows the same address. The site is behind a proxy and the Client IP header plus Trusted proxy addresses are not configured.
- Comments suddenly land in the spam queue site-wide. The honeypot field name, the signed timestamp and the JavaScript token are all derived from the site's authentication salt, so rotating the salts invalidates any page a full-page cache is still serving. Purge the cache. If the salts were not rotated, check the cache age instead: a page older than 4 weeks fails the timestamp check on its own.
- Comments from a few readers always land in spam. "Require JavaScript for comments" does that to any client without JavaScript.
- Registrations rejected as undeliverable. The MX check queried DNS and got no MX and no A record for the domain; it only fails open when the server cannot do DNS lookups at all.
- Cloud sync reports success but nothing of yours appears on the service. Reading the feed is anonymous, so a pull succeeds even when a push cannot: check the queued count, whether the domain is verified, and whether the key has write scope. The status card shows the queue; "Test connection" reports the other two.
- The GeoIP notice says the database is more than 35 days old. With the default source, "Manual upload (.mmdb file)", the "Download / refresh now" button cannot refresh anything: it has no source to download from and does nothing. Unless an earlier download error is still on record, it even reports "GeoIP database refreshed." If a MaxMind or DB-IP attempt failed before the source was set back to manual, the same button reports "GeoIP refresh failed. Check the source settings." instead. Neither message means a file arrived. Upload a new .mmdb file, or switch the source to DB-IP Country Lite or MaxMind GeoLite2 first and then press the button.
- The weekly report never arrives. Press "Send a test report now": it goes out immediately and tells you whether this site can send e-mail at all. The scheduled send also depends on WP-Cron running.
- The statistics look empty or shorter than expected. The default retention is 30 days, and "Clear log" wipes the figures along with the log.
Privacy: what is stored, what leaves the site
With the default settings nothing at all leaves the server. The plugin registers a personal-data exporter and eraser with WordPress, both keyed on the account name in the log rows, and IP addresses can be stored in full, truncated or hashed form and are purged automatically after the retention period.
- Stored locally: the tables wp_msc_log, wp_msc_lockouts and wp_msc_cloud; the options msc_settings, msc_geo_state, msc_db_version, msc_schema_rev, msc_cloud_queue, msc_cloud_state and msc_report_last_sent; and transients prefixed msc_.
- The GeoIP database is kept in a directory under wp-content/uploads named m-security- plus eight characters derived from the site salt, protected by its own .htaccess and index.php.
- Outbound requests happen only when you ask for them: to MaxMind when that source is selected, to DB-IP when that one is, and to the shared feed service when the Cloud module is enabled.
- With sharing on, a report carries an IP address or a SHA-256 hash of an e-mail address plus a category such as bruteforce or spam. Usernames, comment text, form contents and plaintext e-mail addresses are never queued or sent, allowlisted addresses are never reported, and this site's own address and domain are never reported.
- The registration e-mail lookup uses k-anonymity: only the first five characters of the hash leave the site.
- Outgoing e-mail is limited to the optional lockout notice and the optional weekly summary, both through wp_mail.
Deactivating and uninstalling
Deactivating and deleting are two different things here. Deactivation only clears the scheduled jobs: the settings, the log, the lockouts and every permanent ban stay in the database and come back the moment you activate the plugin again. Deleting it from the Plugins screen runs the cleanup routine.
- Deleting the plugin drops the three tables, removes the plugin's own options and its msc_ transients, clears the four scheduled jobs, and deletes the GeoIP directory under uploads.
- What survives: the WordPress core setting comment_previously_approved as you left it on the Spam tab, plus your users, comments and everything else WordPress owns. The plugin touches none of it.
- Ticking "Keep data on uninstall" on the Logs & privacy tab before deleting leaves everything in place, which is what you want when you are moving hosts or reinstalling.
- On a multisite network the three operations behave differently. Uninstall always runs on every site in the network. Activation and deactivation only loop over every site when the plugin is activated or deactivated network-wide; activating it on a single subsite installs the tables and seeds the allowlist on that one site only, and does nothing to the others.
Statistics and reports
Since 1.4.0 the plugin shows what it has actually stopped. An "M Security — blocked attacks" widget on the WordPress dashboard and a Statistics tab (the first tab of the settings screen) chart blocked sign-ins and blocked spam over the last 7 or 30 days, list the busiest day and the top source countries, and show how much work the shared threat feed did. An optional weekly e-mail summary — off by default, enabled on the Statistics tab — arrives on Mondays at 08:00 site time and compares the week against the one before it; a "send a test report now" button confirms delivery works.
Usage telemetry and feedback
Since 1.5.0 the plugin sends an anonymised usage snapshot to black.majevski.com, controlled on the Cloud tab with three states: Off (nothing is ever sent, and no scheduled send exists), Basic (version numbers and which protections are enabled), and Full — the default — which adds the same lifetime counters the plugin shows you. No user names, e-mails, IP addresses, URLs or content ever leave the site; a "what will be sent" preview shows the exact JSON, and a separate checkbox controls whether your site's domain is disclosed. Data is retained at most 180 days after an install goes silent. A "Send feedback" page under the M Security menu delivers bug reports and feature requests straight to the developer — it works even with telemetry off, and attaches diagnostics only when its own checkbox says so.
Blacklisting comment authors
Since 1.7.0 the Comments screen has a "Spam & blacklist" action, under each comment and in the bulk menu. It marks the comment as spam like the native action, permanently bans the author’s IP on your site, and reports the IP and a SHA-256 hash of the e-mail address to the shared blacklist — the plaintext address never leaves your site. Private and allowlisted addresses are never reported, e-mail addresses of your own registered users are skipped, and the action requires the moderate-comments capability. In the other direction, comments (and pingbacks) from authors on the shared blacklist are refused outright while the "Block comments from listed authors" toggle on the Cloud tab is on. A single report does not blacklist anyone globally: another member site must corroborate it first, which protects everyone against mistakes and abuse.
Automatic updates
The plugin checks majevski.com for new releases and offers them through the normal WordPress update screen — the same prompt, changelog popup and one-click install as any other plugin. Checks are cached, never slow a page down, and fail open: if majevski.com cannot be reached, the site simply carries on and tries again later. An update package is only ever accepted from majevski.com over HTTPS, and an older version is never offered. Versions before 1.6.0 predate the update channel, so they cannot see new releases — install 1.6.0 or newer manually once, and every later release arrives on its own.
Want something like this built?
Everything on this page was designed, built and is run by one person. If you need the same for your business, tell me what you have in mind.
Book a call opens in a new tab