WP Opsec

A modular WordPress security plugin built around one idea: block what can be blocked before WordPress finishes loading, and make everything else visible enough to act on.

This site is the development instance. The plugin runs here in log-only mode against real traffic so its detections can be tuned before anything is set to enforce.


Why this exists

The design follows what current data actually says about how WordPress sites get compromised, rather than what is easiest to write signatures for:

  • 91% of the 11,334 WordPress vulnerabilities disclosed in 2025 were in plugins — not core, not themes.
  • ~60% of infections exploit a flaw that already had a patch available.
  • Median time from disclosure to mass exploitation: about five hours.
  • 43% of plugin vulnerabilities are exploitable without authenticating at all.
  • Typical hosting-level defences blocked only 12% of WordPress-specific attacks.

Two consequences shaped the plugin. A human reading a report loses a five-hour race, so the update path is automated rather than merely reported. And because the 2026 supply-chain backdoors survived by creating hidden administrator accounts, detecting that step matters more than detecting the obfuscated code that performed it.


The twelve modules

Blocking the request

  • IP Blocklist — pulls CINS Army, DShield, Spamhaus DROP and FireHOL feeds daily, compiles them into a merged sorted-range file and binary-searches it with no database hit. Also enforces a shared local list that other modules escalate into.
  • Query Guard — signature scanning for SQLi, XSS, traversal and command injection in query strings, plus a deliberately narrower set for POST bodies and file uploads. Optional xmlrpc.php blocking with a Jetpack exemption.
  • Login Guard — per-IP failed-login rate limiting with lockout, escalating repeat offenders to the early gate so a bot stops receiving the login page at all.
  • User Enumeration — blocks ?author=N probing, anonymous REST user listing and oEmbed author leakage. A known-valid username halves the work of a brute force.

Detecting compromise

  • File Integrity — core verified against WordPress.org’s published checksums, which is authoritative even if the plugin was installed after a compromise. Plugins and themes are compared against a local baseline, since no published checksums exist for them.
  • User Watchdog — watches privileged accounts through WordPress hooks, and separately by hourly snapshot diff, which catches an administrator inserted straight into the database by raw SQL where no hook ever fires.
  • Hardening — disables the dashboard file editor, blocks PHP execution in uploads, and finds plugins that hide themselves from the Plugins screen by filtering it.
  • Vulnerability Scan — flags outdated components, plugins pulled from the .org directory, and anything unmaintained for two years or more. An optional WPScan token adds real CVE records.

Closing the loop

  • Auto Updates — installs what the vulnerability scan flagged, rather than waiting for someone to read the report. Only ever adds to what you already enabled per plugin; an explicit exclusion list is the one thing that turns an update off.
  • Security Headers — CSP, HSTS, frame and referrer policy. The two that can genuinely break a site are off by default, and HSTS understands sitting behind a TLS-terminating proxy.
  • Event Log — one filterable table every module writes to, so the answer to “what happened” is not an SSH session and a grep.
  • Telegram Alerts — real-time notification for events above a severity threshold, with a cooldown so a scanner cannot flood the channel.

How it works

Blocking happens before WordPress loads

The enforcement layer is a must-use plugin that runs before the rest of WordPress bootstraps. Modules drop individual check files into it, and a blocked request never reaches theme or plugin code. This is also why alerting is wired directly into the logging function rather than a WordPress action hook: a real block calls wp_die() immediately, so plugins_loaded never fires for exactly the requests most worth knowing about.

Request from the internet must-use plugin gate runs before WordPress loads IP blocklist · query string POST body · uploads · xmlrpc blocked 403 Forbidden theme and plugins never run allowed WordPress loads hook-based modules login guard · user watchdog REST · headers · integrity Event log severity 1–20, filterable every module writes here blocked requests are logged too above threshold Telegram alert real time, rate limited Reserved and private IP ranges, plus an operator-supplied trusted-networks file, are exempt from every blocking path — no feed update can lock you out.

Every event carries a severity, 1 to 20

Severity describes what an event implies, not how confident the detection is. Anything at 18 or above means something already succeeded — a core file changed, an administrator exists that should not. A serious exploitation attempt that was stopped tops out at 17. That distinction is what makes the log sortable into “investigate now” versus “background noise from the internet”.

Info Low Medium High Critical 1 – 4 5 – 9 10 – 13 14 – 17 18 – 20 successful login, baseline taken failed login, author scan, listed IP brute-force lockout, XSS attempt SQLi, webshell upload, RCE attempt core file changed, rogue admin account already happened attempted, and stopped

Nothing enforces until you say so

Every blocking module ships in log-only mode. The intended sequence is to watch the Event Log until you trust what it is catching, then switch that module to enforce. Private and reserved IP ranges are permanently exempt from every blocking path, and an operator-supplied trusted-networks file (search-engine crawlers, monitoring) is exempt too — so no feed update or escalation can lock you out of your own site.


Honest limitations

  • A WordPress plugin only ever sees requests that reach PHP. If a file genuinely exists — an exposed .git directory, for instance — the web server serves it directly and no plugin can intervene. That fix belongs in the server config.
  • Probe detection for sensitive paths depends on the site’s permalink structure routing 404s into WordPress. Where it cannot work, it says so rather than implying coverage it does not have.
  • Baseline comparison for plugins and themes detects change since the baseline was taken — not whether the current state was clean to begin with. Core checksums do not share that limitation.
  • An IP allowlist only helps for services that publish stable ranges. Mobile apps, desktop clients and pingbacks arrive from arbitrary addresses and cannot be selectively permitted that way.

Status

Under active development. Twelve modules implemented and tested against simulated attacks — including an administrator account inserted by raw SQL, a self-hiding backdoor plugin, webshell uploads and a deliberately downgraded plugin. A full code review produced ten findings, all since fixed and individually verified against the specific failure each one described.

Not yet released, and not yet running in enforcing mode on a production site.