Stillward Security

Descripcion

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:

  1. 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.
  2. Location — removal is limited to the places this family actually drops
    files: the three wp-content drop-ins, mu-plugins, PHP files in /uploads,
    the wp-content/cache staging copy, filenames it is known to plant, and any
    file in wp-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.php is never deleted under any
    circumstance.
  3. 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 row md5(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. Duplicate user_login values 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

Installacion

  1. Upload the stillward-security folder to /wp-content/plugins/, or install the ZIP via Plugins Add New Upload Plugin.
  2. Activate the plugin.
  3. 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:

  1. 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.
  2. 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.
  3. 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.

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.

Contributors

Translate “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 writes s: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 loose sc_% 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.