Description
Stillward Security follows one rule: protect, don’t disturb.
Most security plugins break sites by aggressively filtering requests, stripping
form data, whitelisting file types, or forcing strict policies. Stillward ships
with only non-breaking hardening enabled by default. Anything that can affect
how your site works is turned OFF until you knowingly enable it.
Enabled by default (safe on any site):
- Security headers — X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
- WordPress hardening — hide version, generic login errors, disable the file editor, remove head meta leaks
- Block username enumeration — stops ?author=N probes and the public REST users list
- Brute-force login protection — IP lockout after too many failed logins
- Upload protection — blocks executable uploads (.php, .exe…) and disables PHP execution in /uploads (normal media and documents still upload)
Opt-in (off by default — enable knowingly):
- Disable XML-RPC (leave off if you use Jetpack or the WP mobile app)
- HSTS header (HTTPS-only sites)
- Content-Security-Policy (test carefully)
- Strict MIME whitelist
- Custom (hidden) login URL
- Request firewall — SQLi/XSS/traversal detection. It NEVER edits your submitted
data, runs in log-only mode by default, and only blocks if you switch it to
Block mode. - Auto-clean for the Malware Shield. Scanning is on and reports what it finds;
removing anything automatically is your decision. You can also clean once, on
demand, with the «Scan & clean» button.
Malware Shield — and why it will not eat your files
The Malware Shield hunts one specific, self-healing infection (fake db.php /
advanced-cache.php drop-ins, vapor-* / host-*-bridge mu-plugins, injected
theme functions.php, sc_* options and cron). Deleting a legitimate file is
worse than the malware, so three gates must all pass before anything is removed:
- Confidence — only an exact marker unique to this malware family can lead
to a removal. The generic «obfuscated code» heuristic is report-only, because
licence loaders, packers and minified libraries look exactly like that. - Location — removal is limited to the places this family actually drops
files: the three wp-content drop-ins, mu-plugins, PHP files in/uploads,
thewp-content/cachestaging copy, filenames it is known to plant, and any
file inwp-includes/wp-admin/the web root that the official WordPress
checksum manifest says WordPress does not ship.
A file that IS part of a real plugin, theme or WordPress itself is never
deleted — it is reported instead, because an infected real file needs
reinstalling, not erasing.wp-config.phpis never deleted under any
circumstance. - Protected paths — this plugin’s own folder and other security/backup
plugins (which legitimately ship malware signatures in their source) are
skipped entirely.
Everything that is removed is copied into the plugin’s own database table
first — never into another file on the server, because a copy of malware on
disk is still malware on disk. If a removal turns out to be a mistake, download
the copy from the Malware Shield tab and put it back. Removed options and cron
events are backed up the same way and restore in one click. An infected theme
functions.php is never modified or deleted: it is reported, so you can
reinstall a clean copy of the theme.
Developers can force report-only behaviour for any path with the
wpss_malware_auto_removable and wpss_malware_protected_paths filters.
Database Audit — the things a file scanner cannot see
A file scanner is blind to a compromise that never writes a file, and that is
not a hypothetical: an SEO-cloaking campaign ran for four months across eight
sites on one hosting account while hourly scans reported clean, because its code
lived in a plugin’s database table, its configuration in an option, its spam in
wp_posts, and its administrators were inserted straight into wp_users.
The audit runs alongside the file scan and asks three questions that have exact
answers:
- Is there an option named after this site’s own hostname? The malware names
its configuration rowmd5(sha1($host)), so the name is different on every
site and no blocklist can list it — but the same name can be computed here and
looked up. There is no false-positive surface at all. Autoloaded 32-hex option
names in general are raised as a warning. - Was any administrator created by something other than WordPress?
WP_User::add_role() assigns a boolean, so WordPress only ever writes
s:13:»administrator»;b:1. A row built by hand in SQL writes a string
instead. That single difference found four rogue administrators across those
eight sites and produced no false positives. Duplicateuser_loginvalues are
treated the same way — WordPress will not create one. - Does any content belong to a user that does not exist? And were large
batches of posts written within a single second? A legitimate import looks
identical to an injection here, so both are warnings with a one-click «It was
me» that stops that exact fact being reported again.
Nothing found in the database is ever changed automatically. A confirmed rogue
administrator can be demoted to Subscriber with its sessions ended and password
reset — never deleted — and the previous role, capabilities and password hash go
into quarantine first so Restore puts the account back exactly as it was.
Critical findings are e-mailed the moment they are recorded: once per distinct
finding per day, at most twelve an hour, and only for findings that mean
something got in. Routine lockouts and firewall blocks are logged but never
mailed, because an alert that is always noise teaches people to ignore alerts.
Safety guarantees
- The firewall only reads requests — it never modifies or strips POST data, so
it cannot break form nonces or submissions. - Deactivating the plugin is non-destructive: it does not touch wp-login.php,
.htaccess, your options or logs. - No per-request database writes — only real security events are logged.
Screenshots





Installation
- Upload the
stillward-securityfolder to/wp-content/plugins/, or install the ZIP via Plugins Add New Upload Plugin. - Activate the plugin.
- Go to Stillward in the admin menu to review settings. Core protection is already on; enable advanced features only if you need them.
FAQ
-
Will this plugin break my site?
-
That is the one thing it is built not to do. Only non-breaking hardening is on
after activation. Every feature that can change how your site behaves — the
request firewall, Content-Security-Policy, HSTS, the strict MIME whitelist, the
custom login URL and XML-RPC blocking — is off until you turn it on yourself. -
The Malware Shield deletes files. How do I know it will not delete mine?
-
Three gates must all pass before anything is removed:
- The file must contain an exact marker unique to the malware family this
scanner targets. The generic «looks obfuscated» heuristic can only report,
never remove, because licence loaders and minified libraries look identical. - The file must sit in a location this family actually drops into, and must not
be part of a real plugin, theme or WordPress core. An infected real file is
reported for reinstalling, never erased. wp-config.php is never deleted. - This plugin’s own folder, and other security and backup plugins, are skipped.
Everything removed is copied into the plugin’s own database table first —
never onto disk — and can be downloaded again from the Malware Shield tab. - The file must contain an exact marker unique to the malware family this
-
Does the firewall change my form data?
-
No. It never edits, strips or re-encodes a submitted request. It runs in
log-only mode by default and blocks only if you switch it to Block mode. -
Should I disable XML-RPC?
-
Only if nothing depends on it. Leave it enabled if you use Jetpack, the
WordPress mobile app, or any service that publishes to your site remotely. -
Does the plugin contact any external service?
-
One, and only WordPress.org’s own API. When the Malware Shield inspects a file
in wp-includes, wp-admin or the web root, it asks WordPress’s built-in
get_core_checksums() for the official checksum manifest of your WordPress
version, so it can tell a file WordPress genuinely ships from one an attacker
planted. That request goes to api.wordpress.org — the same endpoint WordPress
itself uses for updates — and carries only your WordPress version number and
locale. Nothing about your site, its content or its users is sent. The manifest
is cached, and if the request fails the scanner simply reports those files
instead of judging them.There is no telemetry, no analytics, no third-party service and no registration.
Everything else the plugin records stays in your own database. -
What happens when I delete the plugin?
-
Uninstall removes its options, its log and quarantine tables and its scheduled
tasks, on every site in a multisite network. Accounts you demoted
through the account scanner are deliberately left as they are — silently
restoring an administrator during an uninstall would be dangerous.
Reviews
There are no reviews for this plugin.
Contributors & Developers
“Stillward Security” is open source software. The following people have contributed to this plugin.
ContributorsTranslate “Stillward Security” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
2.3.3
- Repackaged. No functional changes from 2.3.2.
2.3.2
- Activating the plugin no longer scans, removes anything or contacts any
server. It creates its tables, writes the default settings and schedules its
cleanup event, and nothing else. Earlier builds ran a cleanup during
activation, which could remove files and options before the site owner had
agreed to anything. - Auto-clean is now OFF by default. Scans report what they find; removal is
something you switch on, or do once with «Scan & clean». - A database option is never removed because its name matches. Its stored value
must carry an exact malware marker first, so an option belonging to another
plugin that happens to share a name is reported and left alone. - The WordPress root and content directory are now read in one place each, and
the plugins folder is derived from this plugin’s own location.
2.3.1
- The plugins and mu-plugins directories are now read from WP_PLUGIN_DIR and
WPMU_PLUGIN_DIR only. The old fallbacks assumed those folders sit inside
wp-content, which is not true on every install. - Fixed: reported paths were labelled «wp-content/…» even on sites where the
content directory has been renamed. The real folder name is used now.
2.3.0
- Renamed to Stillward Security.
- Quarantined files are now kept in the plugin’s own database table instead of a
folder under /uploads, so no copy of removed malware is ever stored as a file
on the server. Copies left on disk by earlier builds are moved into the
database and deleted on update. - A removed file is no longer written back into place. Its copy is offered as a
download instead, so putting a file back into a core, mu-plugins or theme
folder is a deliberate step taken by the site owner. Options, cron events and
demoted accounts still restore in one click. - An infected theme functions.php is now reported instead of being edited.
- Files larger than 1 MB are never removed, because a verified copy of them
cannot be kept. - Fixed: the WordPress file editor was disabled even with WordPress Hardening
switched off. It now follows that setting, and is disabled by withholding the
editor capabilities instead of defining the DISALLOW_FILE_EDIT constant.
2.2.0
- The scanner now looks at the database, not only at files. A four-month
SEO-cloaking campaign across eight sites on one hosting account was missed
entirely by hourly file scans, for a structural reason: it never wrote a file
worth finding. Its code lived in a plugin’s database table, its configuration
in an option, its spam in wp_posts, and its administrators were INSERTed by
SQL. This release adds the deterministic half of the answer — checks that look
for structural facts WordPress itself cannot produce, so there is nothing to
tune and nothing to guess at. Nothing found in the database is ever removed
automatically. - Hidden configuration in wp_options. The payload config was stored in an
autoloaded option whose name was md5(sha1()) of the site’s own hostname — so
it differed on every site and no blocklist could ever list it. The plugin now
computes the same name from your hostname and simply asks whether the row
exists, which has no false-positive surface at all. Options named
wp_custom_range / wp_custom_filters are flagged the same way, and any other
32-character hex option loaded on every request is raised as a warning. - Administrators WordPress did not create. WP_User::add_role() assigns a
boolean, so WordPress only ever writess:13:"administrator";b:1. Every
account created by SQL injection on those eight sites wrote a string there
instead, because the row was built by hand. That one difference found four
rogue administrators and zero false positives. Duplicated user_login values —
which WordPress refuses to create — are treated the same way. Softer signals
(no e-mail address, a registration date that contradicts the account’s own ID,
never used at all) are reported together as a warning rather than separately
as noise. - Demote & lock. A confirmed rogue administrator can be demoted to
Subscriber with its sessions ended and its password reset, in one click and
without deleting anything. The previous role, capabilities and password hash
go into the quarantine first, so Restore puts the account back exactly as it
was. The plugin refuses to demote the account you are signed in as, or the
last administrator on the site. - Content owned by nobody. 4,565 spam posts carried author IDs with no row
in wp_users. Posts whose post_author matches no user are now reported with the
count and the ID (post_author 0 is left alone — menus legitimately use it), as
are batches of posts sharing one post_date down to the second. A real import
looks identical, so both are warnings with a one-click «It was me» that
silences that exact fact for good. - The activity log learned what the rest of a compromise looks like. It
recorded exactly three kinds of event before: blocked logins, hidden-login
hits and the file scanner. Nothing about users, options, database code,
content volume or web-root changes — so most of that incident had no category
it could have been written under, even in principle. There are eighteen event
types now, and the Activity Log tab lists them all so a missing one reads as a
blind spot rather than a quiet site. - Critical findings e-mail you immediately. Nobody logs into a brochure
site’s admin for weeks, which is exactly the window this campaign operated in.
One message per distinct finding per day (an alert that arrives hourly and is
always the same thing teaches people to delete it unread), at most twelve an
hour, and only for findings that mean something got in — routine lockouts and
firewall blocks are never mailed. - Administrator creation, promotion to a privileged role, and deletion of a
privileged account are each their own logged, notifiable event. Ordinary
subscriber registrations are deliberately not logged: on a shop, they would
bury the one that matters. - Build fix: the release ZIP no longer contains the developer’s local
.claude/
configuration. Dot-files and dot-directories are now excluded from the build.
2.1.4
- New Account Scan tab: every WordPress install under the same hosting user,
in one place. This infection spreads across all sites on a hosting account
and the infected ones write it back into the sites you have already cleaned,
so cleaning one at a time never finishes — and a plugin that can only see its
own site can never tell you that. The scan is read-only: it never deletes,
never touches a database, and never reads database credentials out of another
site’s wp-config.php. Results can be downloaded as a text report. - Access is by administrator capability and nonce — WordPress’s own model.
There is deliberately no separate password: anyone who can reach that screen
is already an administrator and could read a stored password out of the
options table anyway. - Fixed: the scanner reported other security plugins — including older builds
of this one — as infected. Every scanner ships the strings it hunts for, so
scanning one with another finds «malware» in it. On a real hosting account
that lit up seven perfectly clean sites. Known signature-bearing plugin
folders are now skipped, and Stillward’s own source is recognised by content
as well, which also covers renamed folders and failed-upload leftovers.
2.1.3
- Keeps working where the host disables WP-Cron. Several hosts set
DISABLE_WP_CRON and expect a real system cron; where one was never configured,
the hourly sweep silently never ran. The Malware Shield tab now warns when
scheduled events are overdue and gives the exact cron command to add, and the
cheap per-request clean is run from the admin (throttled to hourly) so the site
is not defenceless in the meantime. - The admin-account check no longer only matches adm_ names. Real
attacks also create ordinary-looking administrators with an outside email
address, which sailed straight past. An account is now flagged for review when
it matches this family’s naming pattern, or when two softer signals line up —
an email that is not on the site’s domain, an unusually long machine-looking
username, or appearing recently on a site that is much older. Still never
deleted automatically. - Recency only counts on an established site, so installing the plugin on a
brand-new site no longer flags the owner’s own administrator account. - The account-wide scanner now finds the hosting account root on its own by
walking up from wherever it was uploaded. On cPanel/hPanel only public_html is
reachable over the web, so the script has to sit inside one site while scanning
from several levels above it — previously that meant looking up your own
username first. - The System Report now includes ABSPATH and the likely account root, which is
what the account-wide scanner needs.
2.1.2
- The deep scan is now resumable. A real site with WooCommerce and a page
builder holds far more PHP files than one pass can read on shared hosting, and
the old fixed cap meant the same first slice was re-scanned forever while the
rest of the site was never looked at at all. Each pass now continues where the
last one stopped and wraps around at the end, so a few hourly passes cover
everything. The fixed paths this malware always uses (drop-ins, mu-plugins,
theme functions.php, wp-config.php, the cache copy) are still checked in full
on every single pass and are never subject to the cap. - The coverage notice now names the exact range of files a pass covered and where
the next one resumes, instead of only saying that it stopped. - Files per pass is configurable, default raised from 6,000 to 20,000, with a
25-second walking budget so a slow filesystem pauses on time rather than
running the request out. - New System Report on the Malware Shield tab: one copy-pasteable block with
PHP/MySQL/WordPress versions, OPcache and open_basedir state, cron health
(including an OVERDUE warning when WP-Cron is not firing), scan progress,
findings, settings, drop-ins present and the active plugin list. It contains no
passwords, keys or database credentials. - Fixed the release ZIP. It had been built with a tool that writes Windows
backslashes as path separators, which the ZIP format forbids. PHP’s ZipArchive
then treated the whole path as a single flat filename, no plugin folder was
created, and WordPress reported «Plugin file does not exist.» on upload.
2.1.1
- Malware Shield now clears the compiled-code cache after cleaning. This was
the reason the infection kept coming back: it stays resident in PHP’s OPcache
across worker processes, so deleting the files changed nothing while a live
worker still held the compiled copy and simply rewrote them. Each removed file
is now invalidated individually and the cache is reset at the end of the
request. A full PHP-FPM restart is still required, and the Malware Shield tab
now says so in a recovery checklist. - Planted «core-looking» files are now removed, infected real ones are not.
The family drops files such as feed-atom-framework.php and upgrade-plain.php
into wp-includes/wp-admin to blend in. The shield checks the official
WordPress checksum manifest: a marker-carrying file WordPress does not ship is
removed, while a genuine core file that was injected is only reported, because
it needs reinstalling rather than deleting. Works for new filenames too, not
just the known ones. - Scan coverage extended to the places this family also uses: the web root
(config-main.php), wp-config.php injection, and the wp-content/cache staging
copy. wp-config.php and other load-critical files are never deleted, only
reported. - Known dropped filenames (in themes and plugins as well) are removed when they
carry a marker — two independent indicators rather than one. - Added one more of this family’s markers (the SC_TH «L» variant). Reports, without deleting, any other sc_* option and
any random-looking scheduled event that has no callback function — the latter
is what throws «call_user_func_array(): function … not found» on shutdown. - Activating the plugin on an already-infected site now cleans the known paths
immediately instead of waiting for the hourly scan, with the deep sweep
following five minutes later so activation cannot time out. - Fixed: brute-force protection could be bypassed by forging an IP header.
X-Forwarded-For was trusted unconditionally, so an attacker could send a new
fake IP on every request and never hit the lockout — or forge the site owner’s
IP and lock them out. The connection address is now used unless you enable
the new «Site is behind a proxy / CDN» option, which reads proxy-written
headers instead. The settings screen shows the IP the plugin currently sees so
you can check which setting is right for your host. - Fixed: enabling Hide Login locked administrators out of sub-directory
installs. The secret slug was compared against a path that still contained
the sub-directory prefix, so the login page 404’d while wp-login.php stayed
blocked. The site path is now stripped, and wp-admin is detected correctly when
WordPress lives in its own directory. - Fixed: a lockout renewed itself on every blocked attempt, so sustained
hammering kept the real owner locked out indefinitely. Attempts made during a
lockout no longer extend it. - Hide Login now refuses to turn on if the slug collides with an existing page or
post, and no longer 404s admin-ajax.php / admin-post.php for logged-out
visitors (which broke public contact forms). - The Strict MIME Whitelist is now editable in the UI. It previously locked
uploads to five hard-coded types with no way to change them. - Fixed: files such as «example.com.jpg» were rejected as double-extension
attacks. Only web-executable extensions are now matched inside a filename. - Fixed: attack payloads were HTML-escaped twice, so the activity log showed
«<script>» instead of the request. - The log is now rate-limited per IP and event type, so an attack cannot inflate
the log table one row per request. Added an index on the severity column. - Multisite support: network activation now installs on every site, sites created
later are set up automatically, and deleting the plugin cleans up the whole
network instead of only the current site. - Security headers are now also sent in wp-admin and on the login screen. They
were previously front-end only, because the hook they used does not fire for
admin requests. Content-Security-Policy and Permissions-Policy stay front-end
only, so the block editor and media plugins are unaffected. - Translations now load: added the missing load_plugin_textdomain() call, the
Domain Path header, and a languages/stillward-security.pot template. - Fixed: the Malware Shield could delete legitimate files. Removal now
requires an exact malware marker and a known malware drop location. Files in
wp-content/plugins, wp-content/themes, wp-admin and wp-includes are reported
for review and never deleted. - The generic obfuscation heuristic (previously «gzinflate + a dynamic variable»
— which matches plenty of legitimate packed code) is now report-only, needs a
decoder AND a real execution sink AND an embedded payload before it reports,
and only runs in /uploads, mu-plugins and the drop-ins. It is never applied to
core, plugin or theme files: testing on a stock WordPress showed PHPMailer and
getID3 tripping generic «looks packed» tests, because they legitimately call
base64_decode()/gzuncompress() and are full of \x escapes. A scan of a clean
install now reports nothing at all. - Known-malicious mu-plugin filenames are no longer deleted on the name alone —
the file must also contain a marker, otherwise it is only flagged. - New quarantine: every removed file, option and cron event is backed up to a
private, web-inaccessible folder and can be restored with one click. - Infected theme functions.php is now backed up before the injected block is
stripped, and the rewrite is rejected if it would leave invalid PHP. - Cron cleanup no longer matches generic hashed hook names, and database cleanup
matches exact option names instead of the loosesc_%prefix. - This plugin and other security/backup plugins are skipped by the scanner, so
their bundled signatures can no longer trigger detections. - New Malware Shield admin tab: scan-only and scan-and-clean runs, findings with
the reason each item was or was not removed, and the quarantine list. - Malware Shield settings (enable, auto-clean, quarantine retention) are now
exposed in the UI instead of being hidden options.
2.0.0
- Rebuilt to be safe-by-default and portable across any site.
- Removed the aggressive SQLi word-matching that could block legitimate form and
comment submissions. - Firewall is now read-only, log-only by default, and opt-in.
- Removed per-request «headers sent» logging (no more DB bloat).
- Removed remote wp-login.php download/overwrite from the hidden-login feature.
- Upload protection no longer whitelists MIME types by default (normal files keep
working); it blocks only dangerous executable types. - No longer strips ?ver= from asset URLs (keeps cache-busting intact).
- New settings UI with safe/opt-in/advanced badges and an activity log.
