SMEPlan Security Shield

Description

SMEPlan Security Shield is a free, open-source security plugin built around a practical WordPress operations checklist: it watches the 3 most common attack surfaces (OWASP-class attacks plus WordPress-specific ones, persistence mechanisms, and entry vectors), detects issues with baseline/checksum + signature + thresholded heuristics, and remediates safely (quarantine instead of outright deletion; 1-click rollback).

Key features

  • Batched, checkpointed file scanning: prioritizes mu-plugins, drop-ins, the active theme/plugins, and uploads; never loads the whole file tree into RAM at once.
  • Safe database scanning: keyset pagination (no OFFSET) over options/posts/postmeta; only flags a row when it decodes into an actually executable PHP/JS token, skipping image data URIs.
  • Configuration checks: .htaccess/.user.ini rules that map media extensions to PHP, auto_prepend_file, file/directory permissions, weak salts/keys, unusual cron entries.
  • Baseline/integrity: compares core files against WordPress.org’s official checksums; automatically builds a SHA-256 baseline for every plugin/theme on install/update; 1-click restore of any mismatched core file from a signature-verified WordPress.org package, with the current file quarantined first.
  • Quarantine & rollback: moves suspicious files aside (never deletes), with a full transaction log and 1-click rollback.
  • Per-component backups: a “last confirmed good” snapshot of each plugin/theme, refreshed on every trusted update, checksum-verified before every restore, with a best-effort local tamper-resistance layer.
  • Maintenance mode with a TTL that turns on automatically when remediation touches a hot path or a large batch, and turns itself off once a health-check passes.
  • Hardening: login rate-limit/lockout by IP + IP/username (real IP behind a CDN via trusted proxies), disables XML-RPC pingback + caps system.multicall, security headers (HSTS/X-Frame-Options/CSP Report-Only), controlled auto-updates (low-traffic time window, skips VCS-managed sites, health-check after updating).
  • Multi-layer scan scheduling: WP-Cron + an internal watchdog + an HMAC-signed REST endpoint (for system cron/remote pings) + a WP-CLI command — the schedule keeps running even when WP-Cron is unreliable.
  • Multisite: enumerates every site by blog_id, scanning each site’s own uploads folder and tables.

Not yet in this release (planned for later versions)

  • 2FA (TOTP) and CAPTCHA for the login page.
  • Anonymous telemetry (opt-in).
  • Translations (every string is already wrapped in __(), ready for translators via translate.wordpress.org — no translation is bundled with the plugin itself).
  • Action Scheduler integration for enterprise-grade durable queuing.

Privacy Policy

By default, this plugin does not send any data outside of the site it is installed on. Everything it collects (scan findings, logs, baseline data) stays in the local WordPress database and in a protected local storage folder inside the uploads directory (wp-content/uploads/smeplan-security-shield/, blocked from direct web access).

Two features send data off-site, and both are entirely opt-in — off unless the site admin explicitly sets them up:

  • Alert email: if enabled, a summary of new findings is emailed to the site’s configured admin email address (admin_email) using WordPress’s own wp_mail().
  • Alert webhook: if the admin enters a Webhook URL in Policies, a summary (site URL, alert subject, malicious/suspicious counts, timestamp — no personal or visitor data) is sent as JSON to that admin-provided URL whenever new findings are detected. Nothing is sent anywhere unless the admin fills in this field themselves.

That storage folder outlives the plugin on purpose: deleting the plugin removes its options, cron events and capabilities, but leaves the folder in place so a quarantined file is never destroyed by an uninstall performed mid-incident. See the FAQ entry “What is removed when I delete the plugin?” for the reasoning and for how to remove it yourself.

The plugin does not phone home to any SMEPlan-operated server, does not track usage/analytics, and does not include any third-party tracking or advertising code.

Screenshots

Installation

  1. Upload the plugin to wp-content/plugins/ or install it through the Plugins screen.
  2. Activate the plugin.
  3. Go to Security Shield Wizard to check WP-Cron/loopback and configure trusted proxies if the site sits behind a CDN.
  4. See Security Shield Policies to turn hardening features on/off as needed.

FAQ

Does the plugin delete suspicious files automatically?

No. Files at the “malicious” level are moved into quarantine (wp-content/uploads/smeplan-security-shield/quarantine/), never permanently deleted, and can always be rolled back with 1 click. Files at the “suspicious” level are only recorded, waiting for manual review on the Findings page.

What is removed when I delete the plugin?

Deleting the plugin removes every option it created, its scheduled events, and the three custom capabilities it grants. It deliberately does not remove its storage folder at wp-content/uploads/smeplan-security-shield/, which holds the quarantine, the file baseline, the event log and any component backups.

This is a deliberate choice, not an oversight. Quarantined files are moved, not copied — the folder holds the only remaining copy of anything the plugin took out of the site. Deleting a plugin is easy to do in the middle of handling an incident, or by someone who is not the person investigating, and having that click silently destroy both the only rollback path and the only forensic evidence would be the wrong default for a security plugin. Reinstalling brings the previous quarantine and logs straight back.

To remove it, delete that folder yourself over SFTP or your host’s file manager once you are sure nothing in it is still needed. Read the caution first: the quarantine/ subfolder can contain live malicious files that were pulled off the site. Delete them, do not move them back into place.

Does the plugin automatically edit database content or the .htaccess file?

No. Every finding in the database or in configuration files (.htaccess/.user.ini) is only reported, never auto-fixed, to avoid breaking a site’s legitimate functionality.

Does this plugin change how WordPress updates itself?

No. It never supplies its own update source: there is no bundled update checker, no third-party update server, no filtering of the plugin-information API, and nothing written to the transients core caches available updates in. Everything WordPress installs still comes from WordPress.org, fetched and verified by WordPress itself.

What it does offer — off by default, and only if you switch it on in Policies — is control over when an update WordPress has already found and verified gets applied. Using core’s own public auto_update_core / auto_update_plugin / auto_update_theme filters, it can hold an update back until the low-traffic window you configure, skip it while the site is under load, and skip any plugin or theme directory that is under version control (updating those breaks a deploy). It only ever delays WordPress’s own updates; it never substitutes them.

Automated scanners flag any use of those filters for a human to look at, which is why this is spelled out here.

What environment does it need?

WordPress 6.1+, PHP 7.4+. Works best when WP-Cron runs normally; if the site has DISABLE_WP_CRON set or blocks loopback requests, use system cron/WP-CLI following the instructions on the Wizard page.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“SMEPlan Security Shield” is open source software. The following people have contributed to this plugin.

Contributors

Changelog

Only the most recent releases are listed here, because WordPress.org
truncates this section at 5,000 characters. The complete history — every
release, in full — is in changelog.txt, included with the plugin.

0.7.31

Follow-up fixes from a second full review of 0.7.30.
* Legacy quarantine adoption on reinstall (still broken in 0.7.30): activation stamped the stored DB version straight to current, so on a delete-then-reinstall the one-time adoption of pre-signing quarantine sessions never ran and they stayed permanently un-rollbackable. Activation now runs the adoption first, and a partially-completed adoption is retried instead of being marked done. (H1, H2)
* Scanner could churn without ever finishing a pass while reporting “clean”: a file vanishing mid-scan (e.g. one just auto-quarantined) threw an uncaught error that aborted the whole tick, and the scan checkpoint was written non-atomically, so a crash left truncated state that reset the scan to the start. The vanished file is now skipped, and the checkpoint is written atomically (temp + rename). (M4, M3)
* Alerts are no longer counted as delivered when the webhook returns a 4xx/5xx — only a 2xx clears the buffer — closing a gap in the 0.7.30 alert-buffer fix. (M5)
* The example system-cron command now works on plain (non-pretty) permalinks too, and the REST “scan busy” response now carries a machine-readable error code. (M12, M14)
* Accessibility: the “Latest scan overview” table now marks its column headers with scope. (L1)

0.7.30

Fourteen fixes from a full-codebase security and correctness review of 0.7.29. Each was verified against the source and is covered by the behavioural test suite.
* Multisite recovery: a newly created site’s authenticated-scan secret was silently discarded by the settings sanitizer, and legacy quarantine sessions on subsites (and after a delete-then-reinstall) were never adopted by the signing migration — so they could never be rolled back, with the original files already moved. Both fixed. (rc-07, rc-06)
* Blast-radius limit widened: an unverifiable legacy quarantine session can no longer be restored over any bootstrap PHP file at the top of the WordPress root, anything under mu-plugins/, or a wp-content drop-in — all loaded unconditionally, like the wp-config.php and core paths already blocked. (rc-05)
* Backups: the automatic backup no longer refuses a clean component because a syntax-highlighter .js file contains a short PHP open tag (it is “suspicious”, never quarantined), and a failed deferred backup no longer clears its retry record and strands a component with a stale backup that later restored silently. (rc-08, rc-56)
* Alerts are no longer lost when the first send fails: the durable buffer is cleared only after a channel accepts the alert, instead of before. (rc-57)
* False mismatches: the per-request baseline cache is invalidated on write, so a rebuild-then-scan within one request no longer reports hundreds of just-baselined files as changed. (rc-28)
* The cron health check no longer always reports “problems” on a healthy site; the example system-cron command now works instead of failing a few minutes later; delete-plugin cleanup no longer leaves option rows, scheduled hooks, and (on multisite) network rows behind. (rc-15, rc-12, rc-01)
* Smaller fixes: the mu-plugins backup can now be restored (and unsafe drop-in “restore” buttons are replaced with guidance), the log reader no longer hides the oldest line, the scan I/O cap now works in the flat-file phases, and the generated cron secret uses a shell-safe alphabet. (rc-29, rc-73, rc-74, rc-61)

0.7.29

  • Fix: WordPress 6.7+ logged “_load_textdomain_just_in_time was called incorrectly … translation loading was triggered too early” on every admin request. Smeplan_SS_Admin::init() runs on plugins_loaded — before init — and called a WP_DEBUG-only coverage assertion directly. That assertion reads a map built from __() calls, so every admin page load translated this domain before WordPress was ready. The assertion now runs on admin_init instead; it is a development check and loses nothing by running slightly later. Present since 0.7.21.
  • The 0.7.27 release fixed a second, genuine instance of the same class (the custom cron interval’s display name) but not this one, because the search looked at init()‘s own body rather than following what it calls. The regression test added here counts actual translation calls across the whole call graph, which is the only check that would have caught either.