Bulwarden
Bulwarden
Description
Bulwarden is the site agent, installed on each managed WordPress site. It
exposes a signed REST API (jgm/v1) that a Bulwarden hub calls to read
health and security data, run checks and trigger updates. The hub is a separate
application — it is not part of this plugin — and this plugin talks to no hub
at all until you pair it with one. On its own it sends nothing anywhere.
It is built for the person who is responsible for a site they did not build:
an agency looking after forty of them, or an owner who wants one place that
answers “is this thing all right?” without logging into it.
What it reports
- Site health — WordPress, PHP and plugin versions, pending updates, disk
usage, and WordPress’ own Site Health tests, collected in one snapshot. - A full plugin and theme inventory, with each item’s version, whether it
is active, whether an update is waiting, and whether it came from
WordPress.org at all — which is what lets a hub match it against a CVE feed. - Must-use plugins, which WordPress runs on every single request with
nothing activating them and no way to switch one off from the admin. Nothing
else in wp-admin lists them properly, and dropping a file there is the tidiest
way to install a backdoor. - Configuration-level security checks with a posture score — file editing,
directory listing, XML-RPC, debug output, and the rest of the settings that
quietly decide how exposed a site is. - Unfinished updates. An update that already replaced a site’s files can
still fail to finish, and neither state clears itself: a pending database
schema upgrade that every administrator is sent to and the public site hides
completely, or a leftover.maintenancelock from an updater that died
mid-copy. Both are reported, and both can be finished from the hub by running
exactly what WordPress itself would have run. - Email delivery. WordPress keeps no record of the mail a site sends: when
a contact form stops delivering or an order confirmation never arrives, the
error is raised and thrown away. This counts what was attempted, what failed
and why, and reports which transport is doing it — PHP’smail(), an SMTP
server, or the plugin that configured one. No part of any message is stored:
not the recipient, not the subject, not the body.
What it finds
- A malware scan in three layers. Every core and WordPress.org file is
verified against its published checksums, so a modified file is found
whether or not anybody has written a signature for what was put in it; then
every file is read for the code patterns webshells and injected backdoors
actually use. Findings carry the file, the line and a short excerpt. - Backdoors that arrive dressed as an ordinary plugin — a forged
administrator login, a privileged action gated on a key baked into the file,
an account created from hard-coded credentials, users hidden from the users
list. A plugin absent from WordPress.org has no published files to check it
against, so without this a hostile one surfaced only as unverifiable. - A plugin hiding itself. Malware that conceals a plugin does it by
answering the question before WordPress can — and the hidden plugin is then
also the one plugin whose files are never checked. The scan counts what is on
disk itself and compares. - Server-configuration files.
.htaccess,php.iniand.user.iniare
the one part of a WordPress install that changes what the server does
rather than what WordPress does, and nothing in wp-admin shows them. A file
set to run before every request, a rule that makes uploaded images execute as
PHP, a redirect that only fires for search engines: all reported, with the
line. Nothing is ever changed for you — a good share of those directives are
written deliberately by a host or a security plugin, and the point is that
you get to decide which. - Leftover files. An interrupted backup, a finished restore and a migration
plugin all leave things behind and none of them clean up: a
wp-config.php.bak the web server hands out as plain text, adatabase.sql
in the web root, an installer that overwrites the site with no login. Found,
measured, and removable from the hub — and the site decides what may be
deleted, so a stale list can only ever be refused. - Signatures that keep up. The scan’s pattern list can be updated by your
provider without updating this plugin, so a new detection reaches the site in
hours rather than waiting for a release to travel a whole fleet. A site that
has never been paired scans with the list this plugin ships. - A scan that starts itself. When a plugin WordPress.org does not publish
appears in the plugins or must-use folder, the site starts a scan within
seconds and tells your provider what set it off, instead of waiting for the
next scheduled one. Fake plugins are how most WordPress break-ins arrive, and
a scan is a cheap thing to be wrong about: nothing is blocked, quarantined or
deleted — the site looks, and your provider decides.
What it does
- Remote core, plugin and theme updates, wrapped in maintenance mode so
visitors never see a half-updated site — and taken off the host’s PHP time
limit, so a core update on shared hosting finishes instead of being killed
part-way. - Zero-knowledge encrypted backups. The archive is built and encrypted on
this server, under a passphrase that is never transmitted anywhere;
only the ciphertext is uploaded. Nobody — including whoever runs the hub —
can read a backup without that passphrase. - Restores that fail safely. The same thing in reverse, and it can put back
the database alone if that is all that broke. Every table is rebuilt beside
the live one and swapped in only once it is complete, so a restore stopped by
a timeout or a server limit leaves the database exactly as it was rather than
half-replaced — and picks itself back up where it stopped. What could not be
done is counted and reported rather than discarded, so a restore that ran into
trouble says so instead of reporting success. - Page tracing. Your provider can ask this site to render one of its own
public pages and report which file produced each line of the HTML — core, a
plugin, the theme, a must-use plugin. It answers the question nothing in
WordPress answers: which plugin adds the Google Fonts tag, or the tracking
script, or the markup nobody can find a setting for. The page is never
altered to do it. - WP-Cron on a schedule that actually runs. A site with no visitors runs no
cron, which is how scheduled posts, order emails and licence checks quietly
stop. - A support card on the dashboard naming whoever maintains the site, how to
reach them, and a Request help button that files a ticket with them
directly — attached to the right site, without the owner having to explain
which one. - An activity log on the site itself, append-only, recording everything that
changed this site or sent part of it somewhere — and which side asked. Updates
installed by your provider and anything that failed with them, backups,
restores, scans, repairs, deleted leftovers, a file read off the server, a
traced page, and every change to the credentials, the passphrase or the
provider details. WordPress records what changed and never who asked, and the
answer belongs on your own server rather than only in somebody else’s console.
Pairing
On activation the plugin provisions an API key + secret. Copy the connect
string from the Bulwarden entry in the WordPress admin menu and paste
it into the hub’s “Connect a site” form. Your provider’s support details are
then pushed to the site automatically by the hub and appear on the dashboard.
Security
Hub client requests are authenticated with the per-site API key and an
HMAC-SHA256 signature over the method, route, timestamp, a single-use nonce and
the body. The nonce is what makes a signature good exactly once: it is recorded
when the request arrives and refused if it comes back, so a captured request
cannot be replayed. Timestamps outside a 5-minute window are rejected outright,
which is also how long a nonce has to be remembered for. The secret never
travels on the wire — it lives only in this plugin and in the hub webapp.
Privacy
Bulwarden is a client agent: on its own it sends data nowhere. Pairing this
site with a hub (yours, or your maintenance provider’s) is an explicit,
opt-in action — you choose when to copy the connect string from the Bulwarden
screen in the WordPress admin menu and hand it to that hub. Nothing is
transmitted before that.
Once paired, data only moves in these cases:
- Health, inventory, and security checks. The paired hub reads these from
this site’s signed REST API when it polls or when a check is requested —
WordPress/plugin/theme versions and update status, a full plugin & theme
inventory, disk usage, and the configuration-level security posture score. - Backups, only when triggered (the Settings screen’s “Back up now”
button, or a request from the paired hub): the full site is archived and
encrypted on this server with a passphrase that is never transmitted
anywhere, then the ciphertext is uploaded to the hub. Only someone who has
the passphrase can decrypt a backup — the hub operator cannot read its
contents. - Malware scan results, only when a scan is triggered (admin- or
hub-initiated): findings (type, severity, file path, line, and a short
excerpt) are sent to the hub that requested the scan. - Finishing an interrupted update, when the paired hub asks. Nothing is
sent in that exchange; the site runs WordPress’ own database upgrade, or
deletes an expired.maintenancelock file, and reports the outcome. - Server-configuration files, when the paired hub asks: the contents of
this site’s.htaccess,php.iniand.user.inifiles, so the hub can show
what is in them and when they last changed. These are configuration, not
content — no post, page, media file or user data is read. - Email counters, as part of the health snapshot: how many messages this
site attempted and how many failed over the last seven days, the error text
the mail server returned, and which transport is sending. No part of any
message is recorded — not the recipient, not the subject, not the body. - A traced page, only when the hub asks for one: the HTML of a single
public page of this site, exactly as an anonymous visitor would receive it,
plus the file paths and line numbers that produced it. No file contents are
read and no logged-in view is ever rendered.
Independently of any hub pairing, this plugin verifies WordPress core and
wordpress.org-hosted plugin files by comparing local file hashes against the
public checksum APIs at api.wordpress.org and downloads.wordpress.org.
Those requests carry only what identifies the release to look up — the
WordPress version and locale, and each plugin’s slug and version. No file
contents, hashes, URLs or user data are sent: the reference checksums come
back and the comparison happens here, on this server.
Bulwarden does not call home on activation, does not collect analytics, and
stores data on no server this site hasn’t been explicitly connected to. The
hub itself is a separate service outside this plugin’s control; consult your
provider for how it handles the data described above.
External services
This plugin connects to the services below. Nothing here runs on activation.
Your Bulwarden hub
Bulwarden is the site-side agent for a hub — the management application an
agency or site owner runs to watch and maintain a fleet of WordPress sites. The
hub is a separate product and is not part of this plugin.
The default hosted hub is operated by Bulwarden at https://bulwarden.net/ —
terms of use: https://bulwarden.net/terms/ , privacy policy:
https://bulwarden.net/privacy/ . A hub can also be self-hosted, or run by the
maintenance provider you buy from, in which case this plugin talks to that
provider’s server instead and their terms and privacy policy apply.
No connection to any hub exists until you make one. You pair a site by
copying the connect string from the Bulwarden screen in the WordPress admin
menu and pasting it into the hub; before that the plugin contacts no hub at
all, and it never discovers or chooses one on its own. Once paired, data moves only in these cases:
- When the hub asks for status. It reads this site’s signed REST API and
receives WordPress/plugin/theme versions and update status, the full plugin
and theme inventory, disk usage, the results of WordPress’ own Site Health
tests, and the configuration-level security posture score. - When a backup runs (you press “Back up now”, or the hub requests one).
The site is archived and encrypted here, with a passphrase that is never
transmitted, and only the ciphertext is uploaded. The hub operator cannot
read a backup’s contents without that passphrase. - When a malware scan runs (started by you or by the hub). The findings —
type, severity, file path, line number and a short excerpt — are sent to the
hub that asked for the scan. - When the hub pushes your provider’s support details — their name, logo,
support address and accent colour — on connect and whenever they change
them. Nothing is sent from the site in that exchange.
Every one of those requests is authenticated with this site’s own API key and
an HMAC-SHA256 signature; the shared secret never travels over the wire.
api.wordpress.org and downloads.wordpress.org
The malware scan verifies WordPress core and wordpress.org-hosted plugins
against their published checksums, using WordPress.org’s own public APIs at
https://api.wordpress.org/core/checksums/1.0/ and
https://downloads.wordpress.org/plugin-checksums/. This happens whenever a
scan runs, whether or not the site is paired with a hub.
These requests identify only the release being looked up: the WordPress version
and locale, and each installed plugin’s slug and version. No file contents,
hashes, site URL or user data are sent — the reference checksums are returned
and compared locally.
These are WordPress.org services, covered by the WordPress.org privacy policy:
https://wordpress.org/about/privacy/
Installation
Bulwarden is one half of a pair: this plugin runs on the site, and a hub
runs somewhere else and manages it. Installing the plugin on its own does
nothing until you pair it with a hub.
- Install and activate the plugin as you would any other — upload it under
Plugins → Add New → Upload Plugin, or install it from the directory. - Go to Bulwarden in the WordPress admin menu. The plugin generated an
API key and secret for this site on activation; the screen shows a single
connect string that carries the site name, URL and both credentials. - Copy that string and paste it into your hub’s “Connect a site” form. That is
the whole pairing step — there is nothing to type on this side, and no
hub address to configure here. - If you want encrypted backups, set a backup passphrase on the same
screen. It never leaves this server, and it is required to restore. Keep it
somewhere you will still have it after the disaster you are backing up
against — nobody, including your hub operator, can recover a backup
without it.
Do I need a hub?
Yes, for anything beyond installing it. The hub is separate software: use the
hosted one, run your own, or use the one your maintenance provider runs. Until
the site is paired, this plugin contacts nothing and does nothing.
Upgrading
Updates install like any other plugin. Your credentials, passphrase and settings
survive an update; nothing needs re-pairing.
Screenshots

The Bulwarden screen at a glance: whether the site is connected, whether backups are encrypted, when the last backup and scan ran, and how current the scanner's signature list is.

Connecting the site: the connect string you paste into your hub, and the API credentials behind it.

Backups: a run in progress showing the phase it has reached, its log and a cancel button, above the encryption passphrase and the folders left out of the archive.

The malware scan: what the last one found, grouped by kind, and how much of the site could be checked against the files WordPress.org published.

The activity log: an append-only record of credential changes, backups, restores, scans and repairs, including the ones the hub triggered.

The dashboard support card — who maintains this site, how to reach them, and the Request help button that files a ticket with them directly.
Faq
No. Before you pair it with a hub it contacts nothing at all, and it never
discovers or chooses a hub by itself. After pairing, data moves only for the
things listed under Privacy below — health checks, backups, malware scan
findings — and only to the hub you paired with.
No, provided you set a passphrase. The archive is encrypted on this server
with a key derived from your passphrase, and only the ciphertext is uploaded.
The passphrase is never transmitted, so a hub holds bytes it cannot decrypt.
The same property is why nobody can recover a lost passphrase for you.
The site stops answering its hub, which the hub will notice and report as the
agent going unreachable. Deleting the plugin removes its settings, its audit-log
table and its working directories (see uninstall.php); backups already stored
on a hub are unaffected.
Yes. Everything the plugin puts on screen — the settings page, the support
card on the dashboard, every status message and every scan finding — is
translated, and WordPress picks it up from your site’s own language setting with
nothing to configure. Both German locales are covered: Deutsch and Deutsch
(Sie). The wording is formal throughout, because these screens are read by the
owner of the site rather than by the agency that maintains it.
The plugin runs per site. A network-wide database upgrade is reported but not
performed — core’s own network upgrade screen walks every site in the
network, and that is where it belongs.
The file-integrity check compares every core and WordPress.org-hosted file
against the checksums WordPress.org published for that exact release. A caching
or optimisation plugin that rewrites a stylesheet produces the same mismatch a
backdoor does, which is why “this file no longer matches what was published” is
reported as a fact rather than a verdict, and why non-executable files are
warnings rather than critical findings. Your hub can show you the actual diff.
Nothing on page loads: there is no front-end code path. Backups and malware
scans are real work and run in the background across WordPress cron ticks, to a
time budget, so they do not block visitors. Health checks are answered on demand
when the hub asks.
PHP 7.4 or newer and WordPress 6.0 or newer. Backups additionally need the
sodium extension (encryption) and either zlib or the zip extension
(archiving); the Settings screen tells you if either is missing.
Reviews
Changelog
1.28.2
- Fix: the progress of a file restore is now reported correctly when it
finishes. The step that writes a backup’s files back ended by reporting
that it had written none of them — a completed restore showed
“0 of 93,680 files, 0%” beside a count of the files it had just written.
The two figures were also counted differently: the total included the
backup’s database dump and its description alongside the files, so it could
never be reached however many files were restored. It now counts the
backup’s files, and every one of them is reported either as written or as
deliberately left alone — this site’s own wp-config.php, Bulwarden
itself, and WordPress’ drop-ins are never overwritten, and your hub now says
so instead of leaving the difference unexplained.
1.28.1
- Fix: a restore no longer spends most of its time waiting to be asked to
carry on. A restore runs in rounds, and at the end of each one the plugin
asked WordPress’ scheduler to come straight back for the next — except that
the request was made from inside the scheduler, which refuses it. So the work
only continued when a visitor happened to arrive, or after a three-minute
safety timer, and on a quiet site — which is most sites being restored — a
restore took hours of waiting for minutes of work. On sites where WordPress’
own scheduler is switched off in favour of a real cron job, which is the
usual advice for a site that matters, nothing continued the restore at all
between those cron runs. Each round now asks for the next one directly.
1.28.0
- New: a banner on a site that has the plugin but is not connected to
anyone. Until a site is paired with a maintenance hub the plugin does
nothing at all — no backups, no scans, nothing watched — and from the
dashboard that is indistinguishable from a plugin quietly working. Anyone who
can install plugins now sees a notice saying so, with the way to connect the
site one click away, and it can be put off for a month at a time. The support
widget on the dashboard no longer says Bulwarden maintains a site that nobody
has agreed to maintain. - New: a way to ask Bulwarden for help when there is no maintenance provider
at all. The Request help button on the dashboard writes to whoever looks
after the site, which is no use to somebody who installed this and has nobody.
The plugin’s own settings screen now carries a short form that reaches
Bulwarden directly, and says exactly what it sends: the site’s address, its
WordPress and PHP versions and the reply address typed into it. Nothing else,
and no keys. - New: the hub is told whether a backup encryption passphrase has been set —
the fact that there is one, never the passphrase itself, which stays on this
site and is never sent anywhere. Without one every backup stops before it
starts, and an agency previously found that out from a failed run at three in
the morning rather than from the setup nobody finished. - New: five more malware detections, from a study of 400,000 production
webservers (Kasturi et al., Mistrust Plugins You Must): code that redefines
a built-in PHP function instead of loading the real one, a function name
assembled from fragments and then called, a file written to disk, made
executable and run, code that deletes the file it ran from, and the branding
“nulled” plugin marketplaces stamp into the paid plugins they give away with
something added.
1.27.3
- Fixed: a restore of a large backup no longer leaves the site stuck showing
“Briefly unavailable for scheduled maintenance”. Downloading and decrypting
a backup take as long as they take, and a round of work that had spent its
whole time doing that went on to start restoring the database anyway — which
begins by switching the site into maintenance mode. It then managed a single
instruction before stopping for the round, so the maintenance mode bought
nothing; and because WordPress checks for maintenance mode before it loads
any plugin, a round that did not live long enough to switch it back off took
the site’s own timer down with it — the timer being the only thing that could
start the next round. The site sat unavailable, and the restore sat still,
until WordPress itself gave up on the maintenance flag ten minutes later. A
round that is out of time now stops cleanly between phases, and maintenance
mode is switched off the moment the database work pauses rather than at the
very end of the round.
1.27.2
- Fixed: restoring a backup onto a different site now rewrites its addresses
to this site’s. With “rewrite addresses” ticked, the restore replaced the
backup’s web address with the address it read from the site — but it read it
after the backup’s database was already in place, so it was reading the
backup’s own address back. A backup moved to a new domain either had nothing
rewritten at all, leaving the restored site pointing at the domain it was
copied from, or had every address in the database rewritten fromhttp://to
https:// on that old domain. The restore reported success in both cases.
This site’s addresses are now recorded before the restore starts and used as
the target; if they could not be recorded, the addresses are left alone and
the restore says so instead of guessing. - Fixed: a backup now records the site’s own stored web address rather than the
address of the internal request that happened to start it, so the restore
searches for the address that is really in the backup.
1.27.1
- Fixed: a long restore of a backup that predates this plugin now finishes.
Restoring the database replaces the list of active plugins with the backup’s,
and a backup taken before this plugin was installed does not list it. On a
site big enough to need several rounds of work, the round after the swap
loaded WordPress without the plugin, so nothing was left to carry on and the
restore sat half-done until the hub gave up. The plugin now puts itself back
the moment the table is swapped in, in the same round. - Changed: a restore never writes WordPress’ drop-in files
(object-cache.php,db.php,advanced-cache.phpand the rest of that
family inwp-content). They are loaded before any plugin and are tied to
the server they were written on — a cache server address, database hosts,
absolute paths — so a backup’s copy restored onto another server took the
whole site down before anything could repair it. The copy already on the site
is kept, and the restore log says which files were left alone.
1.27.0
- Fixed: a large restore no longer stops part-way through the database.
Restoring the database replaces the WordPress options table, and the
restore’s own progress record and its “carry on” timer were rows in that
table — so on a site big enough to need several rounds of work, the round
that swapped the table in was the last one that ever ran. The site sat
half-restored, the dashboard showed the last progress it had heard of, and
if the backup came from another site, the hub could no longer reach this one
at all. The restore now puts its own state and this site’s own credentials
back the moment the table is swapped in, and carries on. - Fixed: each round of a long restore now starts the next one immediately
instead of waiting for a timer, which roughly halves the wall-clock time of a
restore that spans many rounds — and no longer depends on a visitor arriving
to trigger it. - Improved: restoring a large database is many times faster. Backups now
write rows in batches rather than one statement per row, and the restore
replays them inside transactions, so a database that used to trickle in at
about a thousand rows a second now goes back at the speed of the disk.
Backups made by earlier versions restore exactly as before. - Improved: a restore interrupted by the hosting provider (a killed process, a
dropped database connection) now resumes exactly where its last saved
progress left off. Previously the rows written since the last save were
replayed on top of themselves, which on a bad day was enough errors to
abandon the restore. - Improved: a restore that resumes after a pause keeps the database session
settings the backup asked for (character set, foreign-key checks, the rule
that keeps a row with id 0 from being renumbered). - Improved: two rounds of one restore can no longer run at the same time.
- Improved: the hub’s restore page now says when it could not ask the site for
progress, and why, rather than showing the last answer as if it were live. - Developer note:
BULW_RESTORE_TICK_BUDGET(seconds) shortens a restore’s
per-round budget so that a multi-round restore can be reproduced on a small
test site.
1.26.0
- New: the scan now reports program files stored on your site. A malware scan
reads code — PHP, JavaScript, page templates — so an installer or a program
sitting in your media library was invisible to it. That is the commonest thing
a compromised WordPress site is actually used for: not defacing the site, but
quietly hosting a file that is mailed out to other people, until the domain is
flagged by Google Safe Browsing or the host suspends the account. These are now
listed on the security tab. - New: a program stored under a name that says otherwise — a Windows or Linux
program saved as.jpg,.pdfor.mp4in the uploads folder — is reported as
a critical finding. There is no innocent reason for one, and it is exactly how
such a file is kept out of sight. - New: a program file found inside
wp-adminorwp-includesis reported as
critical. WordPress itself ships none, in any language. - New: every one of these findings carries a SHA-256 checksum of the file, shown
next to it in your provider’s dashboard. That is the part your provider can act
on: it identifies the file itself, so it can be checked against a malware
database and recognised if the same file turns up on another site. - Note: an ordinary program file elsewhere on the site is reported for
information only. It never marks the site as infected and never triggers an
alert — a business that distributes its own software has every right to store
one, and the scan says so rather than crying wolf.
1.25.1
- Fix: a restore no longer hands this site another site’s identity. WordPress
keeps this plugin’s settings in the database, so restoring a backup made on a
different site replaced this site’s connection credentials with that site’s —
and from that moment your provider’s hub could not reach this site at all: no
health checks, no cron, no scans, no backups. Putting it right meant logging in
and pairing the site again by hand. The credentials, the hub address and the
backup passphrase are now carried across a restore and put back the instant the
database is in place. Everything else in the backup’s settings is restored as
before. - Fix: the backup passphrase was replaced the same way, so the next backup was
encrypted with a passphrase recorded for a different site — something nobody
would discover until the day they needed it. - Fix: a backup no longer carries its own “restore in progress” marker, which
could drop one run’s state onto a site part-way through another.
1.25.0
- New: a backup can now be restored onto a site whose database tables are named
differently. WordPress puts a short prefix in front of every one of its table
names, and it is rarely the same on two installs — so restoring a backup onto
a staging copy, or onto a site being rebuilt after a break-in, used to be refused
outright. Your provider can now ask for the tables to be renamed as they are
restored, which leaves the site reading its own names and needs no change to
wp-config.php at all. Everyone’s role and personal settings are moved across with
them, so nobody loses their login. - Change: the message shown when the names do not match no longer recommends
something harmful. It used to suggest editing wp-config.php to match the backup.
On a site that is up, that stops WordPress finding its own database, takes the
site down, and stops this plugin running — which removes the only thing able to
perform the restore. The message now explains the two safe ways forward instead.
1.24.1
- Fix: a restore that stopped explained itself in punctuation codes. The messages
this site sends your provider when a restore cannot go ahead — the reason it
stopped, and the database errors behind it — were encoded for a web page here
before being sent, and then encoded a second time by the page that displayed them.
Every apostrophe and quotation mark in the one line meant to explain the failure
arrived as a run of digits and semicolons, including the table names in a database
error, which left the explanation unreadable exactly when it was needed. The
messages are now sent as written.
1.24.0
- New: a restore can now be watched while it runs. Putting a site back can
take an hour on a large one, and until now your provider’s hub learned nothing
at all until it had finished — a spinner, and then an answer. This site now
reports every step of a restore as it goes: checking, downloading, decrypting,
the database, the files, the addresses, and the finish, each with how far
through it is and how long it has taken, alongside the run log as it is
written. - Change: a step that will not run says so from the start, and says why — a
database-only restore is not quietly “about to” restore your files.
1.23.0
- New: the malware scan knows eighteen more kinds of backdoor. They were
written against real ones: the plugins compromised on wordpress.org in June
2024 and April 2026, the two backdoored plugins found there in 2026, and the
fake plugins that plant a hidden administrator, rewritewp-cron.php, take a
role from the request, or unpack code fetched from a server nobody published.
Each check pairs the dangerous action with the thing that makes it
illegitimate, so an honest plugin doing the same work is not reported. - Fix: eight existing checks reported honest code as malware. A code comment
mentioning a shell command, a full-width video frame, a compressed setting, an
embedded font, a temporary-login plugin creating its account from variables,
and a plugin’s own test files were each being flagged. Measured against the
250 most popular plugins and WordPress core, those checks now report nothing
there. Your provider’s hub refuses to install an update it considers infected,
so this also unblocks updates it was wrongly refusing.
1.22.2
- Change: nothing on your site changes in this release. It exists because the
plugin’s description here had fallen behind what the plugin actually does, and
the directory shows that text from the released version rather than from the
latest one — so correcting it takes a release. - Change: three features that shipped in 1.20.0, 1.21.0 and 1.22.0 were described
only in this changelog and not in the description anybody reads first: the
activity log on your own site, the scan the site starts for itself when a plugin
nobody published appears, and what a restore now does when it cannot finish. - Change: the security section described how requests between this site and your
provider are signed, and had been describing the form used before 1.11.0. The
signature has covered a single-use value per request since then, which is what
makes a captured request useless rather than merely short-lived. - Change: the screenshots in the directory listing were taken nine releases ago,
before the support card had a Request help button or a telephone number. They
are re-taken from this version.
1.22.1
- Fix: backups of sites larger than 4 GB could not be completed. Past that size the
archive was written in a form no unpacker can read, so the check that runs on every
finished archive rejected it and the backup was reported as failed with “the finished zip
could not be re-opened (code 21)”. Smaller sites were never affected. On a site whose
hosting is missing PHP’szipextension that check cannot run, so an affected backup may
have been stored instead of failing — those cannot be restored, and should be
replaced with a fresh backup after updating.
1.22.0
- Improve: a restore that cannot finish no longer leaves your site half-restored. Each
table is now rebuilt alongside the live one and swapped in only once it is complete, so a
restore interrupted by a timeout, a server limit or an error leaves your database exactly
as it was instead of partly replaced. Long restores also pick themselves back up where
they stopped rather than starting over, and your site is put into maintenance mode only
while the database is actually being written. - Improve: a restore now says when something went wrong. Failed database statements are
counted and reported instead of being discarded, and a restore that could not create or
replace your tables stops immediately and explains why rather than finishing and
reporting success. Your provider’s dashboard shows the difference. - Improve: checks before a restore starts, so it fails before it can do damage. Free
disk space, the database permissions the restore needs, whether the downloaded backup
matches what was stored, and whether the backup’s tables match this site’s are all
verified first. An incomplete download is now reported as an incomplete download rather
than as a wrong passphrase. - Improve: restores can optionally remove files the backup does not contain, for
recovering from a hack rather than from a mistake. Off unless your provider asks for it,
and never applied to your uploads. - Improve: after a restore, the site is put back in working order: caches are cleared
(so you see the restored site and not the old one), plugins and themes the restored
database names but that are no longer installed are deactivated rather than breaking the
site, and a pending WordPress database upgrade is completed. - Improve: backups and restores can no longer run at the same time. A backup started while
a restore was in progress could previously store a copy of a half-restored site. - Improve: web addresses are rewritten more completely on a restore — the WordPress
address and the site’s folder path as well as the home address — and the rewrite can
no longer skip rows on large tables or be stopped by unusual stored data. - Improve: database triggers and stored routines are now included in backups and restored
with them. - Fix: the number of files a restore wrote is shown again on your provider’s dashboard.
- Fix: files whose names contain two dots in a row (
photo..2019.jpg) are no longer
skipped by a restore.
1.21.0
- Improve: the Activity log now records the things it was missing. Updates installed on
this site by your provider are written down — what was updated, and anything that
failed — and so are a file being read off this server, a change to the provider
details shown on your dashboard, and a change to the address this site reports to.
Between them those were the actions with the biggest effect on the site and the least
trace of who asked for them. Nothing about how the plugin works changes; the log simply
answers more of the questions people bring to it.
1.20.0
- Add: a plugin that appears out of nowhere is now scanned immediately. When a plugin
turns up on your site that WordPress.org does not publish — a premium plugin you
installed by hand, or something nobody installed at all — the site starts a malware
scan itself, within seconds, and tells your provider what set it off. Fake plugins are
how most WordPress break-ins arrive, and until now the first look at one waited for the
next scheduled scan. Nothing is blocked, quarantined or deleted: the site looks, and your
provider decides. Anything appearing in the must-use plugins folder counts too, since a
file put there runs on every request with nothing to activate it and no way to switch it
off.
1.19.0
- Add: German. Every screen this plugin adds — the settings page, the dashboard
support card, every status message, every scan finding and every error — is
translated, in both Deutsch and Deutsch (Sie). Nothing to configure: WordPress uses
your site’s own language setting. The wording is formal throughout, because these
screens are read by the person whose site it is rather than by the agency maintaining
it. - Add: a page trace now lists the page’s resource hints — the
and<link rel="dns-prefetch">tags — and says which
plugin asked for each one. That tag is the reason a page reaches out to a third-party
server before it has drawn anything, it is what a webfont privacy check flags, and it
was the one thing on a traced page the trace could not name: nothing enqueues it, and
WordPress prints it itself, so tracing the bytes only ever answered “WordPress”. Hints
added through WordPress’ own filter now carry the callback that added them; hints
printed straight into a theme template are found in the page and labelled with the file
that printed them. - Add: server-configuration checks. The
.htaccess,php.iniand.user.inifiles
inside your WordPress folder are now read and reported to your provider, with a history
of every time one of them changed. These are the one part of a WordPress install that
changes what the server does rather than what WordPress does — nothing in the
WordPress admin shows them, and a single line in one can outlive every other trace of a
break-in. Flagged: a file set to run before every request, a rule that makes uploaded
images execute as PHP, a redirect that only fires for search engines or for visitors
arriving from one, and aphp.inisitting in the media library, which has no legitimate
reason to exist. - Note: nothing is ever changed on your site. A good share of these directives are written
deliberately by a host or a security plugin, so the point is that you get to see them and
decide — not that they are removed for you. A folder correctly set to refuse PHP is
reported too, as good news rather than as a problem. - Add: the scan’s signature list can now be updated by your provider without updating this
plugin. A new detection reaches your site within hours instead of waiting for a plugin
release to travel a whole fleet — and a signature that turns out to fire on a
legitimate plugin can be switched off just as quickly. A site that has never been paired,
or whose provider is unreachable, keeps scanning with the list this plugin ships. - Add: email delivery is now monitored. WordPress keeps no record of the mail a site
sends: when a contact form stops delivering, or an order confirmation never arrives, the
error is raised and thrown away and the site goes on looking perfectly healthy. This now
counts what the site tries to send and what fails, keeps the reason the mail server gave,
and reports which transport is doing it — PHP’smail(), an SMTP server, or the
plugin that configured one. Your provider sees it alongside everything else. - Note: no part of any message is recorded. Not the recipient, not the subject, not the
body — only how many were attempted, how many failed, and why. And “this site has
sent no email” is reported as exactly that rather than as a fault: a site with no forms
and no shop legitimately sends nothing for months. - Add: a malware scan can now be cancelled from your provider’s hub. A full scan reads
every file on the site across several scheduled runs, so on a large or slow one it can
sit half-finished for a long time — and until now nothing could stop it, including
the next scan, which was refused because a run was already going. Cancelling stops the
walk on the site and lets a new scan start straight away. - Add: the backup passphrase field now shows how strong the passphrase is as you type.
Nothing is enforced — a backup passphrase is not a login, and nobody, including
your provider, can recover a backup without it, so being locked out by a rule you did
not expect would be far worse than a weak one. The meter simply says what helps:
length beats punctuation, and four unrelated words reach the top of the scale where
a short password with symbols in it does not. - Fix: a remote update could be killed part-way through by the host’s PHP time limit. A
core update or a batch of plugin updates is downloaded, unpacked and installed inside a
single request, and thirty seconds — the default on a great many shared hosts
— is not long enough. The update is now taken off that clock and detached from the
connection that asked for it, so it always runs to completion even if the hub stops
waiting; the hub, for its part, now re-checks the site rather than reporting a slow
update as a failed one.
1.18.0
- Add: the malware scan now recognises the backdoors that arrive dressed as an ordinary plugin, not just the classic obfuscated webshells. A plugin absent from WordPress.org has no published files to check it against, so until now one like this surfaced only as unverifiable — never as malware — however plainly hostile its code. The scan now flags, as critical, a plugin that forges a login as an administrator it picked for the caller, that gates a privileged action on a secret key baked into the file, that creates an administrator from hard-coded credentials, that hides accounts from the users list, or that runs request data through a dynamic function call.
- Note: each of these is matched by the thing that makes it illegitimate, not by an API a healthy plugin also uses — a login plugin that sets a session after checking a password is untouched, and WordPress’ own installer, which creates the first administrator on every site, is not mistaken for a planted one. The false positives it must not raise are pinned by a test alongside the detections themselves.
1.17.1
- Fix: an untouched WordPress could be reported as having a missing or modified core file. A non-English WordPress installs the readme and licence in its own language too —
liesmich.htmlon a German site,licenza.htmlon an Italian one, eight more names across the languages — and only the Englishreadme.htmlandlicense.txtwere treated as the documentation they are. So deleting them, which most hardening guides tell you to do and a good many hosts already have, was reported as a missing core file, and a copy edited to hide the version number or rewritten in transit was reported as a critical modification. Neither could be cleared: the file’s own language package stopped shipping it years ago. - Change: the release’s documentation is now recognised by what it is — a readme or licence sitting in the site root — rather than by a list of the English names, so the same is true of every language and of any name a future one uses. Files inside
wp-adminandwp-includesare unaffected: nothing legitimately rewrites one of those, whatever its extension, and a modified one is still a critical finding. The skipped files are still pattern-scanned for malware exactly as before.
1.17.0
- Add: page tracing. Your provider can now ask this site, from their hub, to render one of its own public pages and report which file produced each line of the HTML that came back — core, a plugin, the theme, a must-use plugin, a drop-in. The question it exists to answer is the one nothing in WordPress answers: which plugin adds the Google Fonts tag, or the tracking script, or the markup nobody can find a setting for. Styles and scripts are reported separately with the callback that enqueued each one, because those tags are printed by WordPress itself and naming WordPress helps nobody.
- Note: the trace never alters the page. Nothing is injected into the output, no marker and no comment; the attribution travels beside the HTML as byte offsets, so what is measured is exactly the page a visitor would have received. Two things it reports honestly rather than papering over: a page served straight from a page cache never reaches this plugin at all, and a plugin that captures whole sections of the page into its own output buffer collapses everything inside it to one line. Both are said out loud rather than guessed around.
- Note: a trace runs only for a request carrying a single-use token this site minted seconds earlier for one specific page of its own, and the token is spent before the page renders. Only paths on this site can be traced, no file contents are ever read or sent, and every trace …