=== Stillward Security ===
Contributors: adnanali32038
Tags: security, firewall, malware, brute force, hardening
Requires at least: 5.0
Tested up to: 7.1
Requires PHP: 7.2
Stable tag: 2.3.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Lightweight, safe-by-default WordPress security. Hardens your site without interfering with normal behaviour. Aggressive features are opt-in.

== 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:

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.


== Installation ==

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.

== Frequently Asked Questions ==

= 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.

== Screenshots ==

1. The Protection tab. Every feature carries a Safe, Opt-in or Advanced badge, and core protection is already on after activation.
2. Malware Shield. Scan-only and scan-and-clean runs, plus a plain account of what a plugin alone cannot fix after an infection.
3. Findings and quarantine. A clean install reports nothing at all, and anything the shield does remove is kept as a copy you can download again.
4. Account-wide scan. Read-only: it looks at every site under the same hosting user and never changes anything outside this one.
5. The activity log, and the full list of event types it can record.

== 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_<hex> 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
  "&lt;script&gt;" 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.

== Upgrade Notice ==

= 2.3.3 =
Repackaged release; identical in behaviour to 2.3.2.

= 2.3.2 =
Activation no longer removes anything, auto-clean is off by default, and an
option is only removed when its value carries a malware marker.

= 2.3.1 =
Correct handling of installs that move or rename wp-content, the plugins
directory or mu-plugins.

= 2.3.0 =
Quarantined files move from disk into the database, restoring a removed file
becomes a download, and the file editor now follows the WordPress Hardening
setting.

= 2.2.0 =
Adds database and account scanning, quarantine with one-click restore, and much
stricter malware-removal gates. Recommended for all users.

= 2.1.0 =
Malware Shield no longer removes a file on a generic heuristic alone.

= 2.0.0 =
Major rewrite: safe-by-default, firewall is now opt-in and never edits requests.
