Guide

WordPress wp-config.php was modified — how to detect unauthorized changes

You opened your WordPress site this morning and something is off. Maybe redirects to a sketchy domain. Maybe a strange admin user you do not recognise. Maybe nothing visible — just a gut feeling. You SSH in, run ls -la wp-config.

1. Problem

You opened your WordPress site this morning and something is off. Maybe redirects to a sketchy domain. Maybe a strange admin user you do not recognise. Maybe nothing visible — just a gut feeling. You SSH in, run ls -la wp-config.php, and the modification timestamp is from 03:14 last night. You did not deploy anything last night.

Your first thought: wp-config.php modified — but by who, and when exactly?

You search "wordpress wp-config changed by itself" and "wordpress site hacked wp-config" and find a wall of generic security checklists. None of them tell you what actually changed in your file, when it happened relative to admin logins, or whether the change is the cause or the consequence of the breach.

The WordPress dashboard shows nothing. The hosting panel shows nothing. The uptime monitor is green. Everything looked fine before. There is no clear explanation anywhere a normal admin tool would surface.

This is the silent failure mode wp-config.php modifications live in. You only notice once damage is done.

2. Impact

wp-config.php is the most security-critical file on a WordPress install. It contains:

  • The database credentials (DB_USER, DB_PASSWORD, DB_HOST)
  • The eight authentication keys and salts
  • The table prefix
  • Often custom constants like WP_DEBUG, DISALLOW_FILE_EDIT, AUTOMATIC_UPDATER_DISABLED
  • Custom PHP that runs on every request, before plugins, before themes, before WordPress core

An attacker who can write to wp-config.php can:

  • Inject a PHP backdoor that executes on every page load — invisible to plugin scanners
  • Add a define('WP_AUTO_UPDATE_CORE', false); to keep the site exploitable
  • Replace AUTH_KEY and friends to invalidate everyone's sessions and force a password reset flow they control
  • Switch DB_HOST to a logging proxy and exfiltrate credentials of every login
  • Add a wp_*_options-injected redirect that runs before any security plugin loads

Compromise of wp-config.php is not a "broken plugin" event. It is a foothold. By the time your visitors see the symptom — redirects, fake admin notices, SEO spam — the attacker has had hours or days of unobserved access. Lost revenue, tanked search rankings, and the slow horror of explaining to a client why their checkout page now sends Visa numbers to a server in another country.

The reason this drags on is not that the change is hidden. It is that nothing was watching the file.

3. Why It’s Hard to Spot

WordPress has no native file integrity monitoring. None. Core does not hash its own files at runtime, does not watch user-writable files, and surfaces nothing in the dashboard when wp-config.php changes. The "Site Health" screen will not flag it. There is no admin email when the file's mtime jumps.

Uptime monitors check that a URL returns 200. A site with an injected backdoor returns 200 perfectly. A redirect that only fires for Googlebot or for visitors with a Referer: header from Bing returns 200 to your monitor.

Most security plugins do scan files, but they scan on a schedule (often daily) and the dashboard surfaces results inside wp-admin — which means the attacker, who now has admin, can disable the scanner before you log in. Worse, the scanner itself runs under WordPress, after wp-config.php has loaded, so a malicious constant injected before the scanner starts can simply turn it off.

File modification time is unreliable: any attacker with shell access runs touch -t to backdate the file. Disk usage reports won't help because the file is tiny. Hosting backups give you yesterday's version, but nobody compares them automatically.

The result is a class of compromise where the only evidence is a 388-byte difference in a file nobody opens, and the only way to notice is for something outside the WordPress process to be hashing it on a schedule and shouting when the hash changes. That is the gap wp.integrity exists to close.

4. Cause

When wp-config.php is modified, Logystera emits a wp.integrity signal. The plugin maintains a SHA-256 hash of every file in a watched-paths set. On a scheduled scan (and on demand after admin actions), it re-hashes those files and compares against the last known-good baseline. A diff produces one signal per changed file with the payload:

event_type: wp.integrity
payload:
  path: wp-config.php
  change_type: modified         # or: created, deleted
  old_hash: 9f86d081884c7d65...
  new_hash: e3b0c44298fc1c14...
  size_before: 4831
  size_after: 5219
  mtime: 2026-04-26T03:14:22Z
  scanner: scheduled            # or: manual, post-admin-action

That is the mechanism. Not "WordPress detected a problem." Not "your scanner reported something." A wp.integrity event is a deterministic statement: the bytes of this file at this path changed between scan T and scan T+1, and here are the hashes to prove it.

Internally, the plugin watches a small, opinionated set: wp-config.php, .htaccess, index.php, wp-load.php, and the wp-content/mu-plugins/ directory. These are the highest-leverage files an attacker (or a misconfigured automation) touches. The supporting wp.integrity signal for .htaccess is included because most successful wp-config.php injections are paired with a .htaccess rewrite to redirect specific user-agents or referrers — they almost always travel together.

The correlation that makes the signal useful is the auth.attempt signal stream. Every successful and failed admin login is timestamped. When you see a wp.integrity event for wp-config.php at 03:14:22Z, you can ask: was there an auth.attempt with outcome=success from any IP in the preceding two minutes? If yes — that account is the entry point. If no — the attacker bypassed wp-admin entirely (FTP, hosting panel, supply-chain plugin, exposed XML-RPC).

5. Solution

5.1 Diagnose (logs first)

Diagnosis starts from the wp.integrity signal and works outward. If you do not yet have the signal stream, run the equivalent commands by hand on the server.

Step 1: Confirm the change and capture a diff.

# Compare current wp-config.php against the most recent backup
diff /var/backups/wordpress/wp-config.php /var/www/html/wp-config.php

# If you don't have a backup, dump current state for forensics first
cp /var/www/html/wp-config.php /tmp/wp-config.captured.$(date +%s).php
sha256sum /var/www/html/wp-config.php

The hash output is what wp.integrity reports as new_hash. If you have Logystera's signal stream, you already have both old and new hashes — skip to Step 2.

Step 2: Find when the file actually changed.

# Last modification time (untrusted — attacker may have touched it)
stat /var/www/html/wp-config.php

# Inode change time — harder to forge, requires root
stat -c '%Z %n' /var/www/html/wp-config.php

# Filesystem audit log, if auditd is configured
ausearch -f /var/www/html/wp-config.php --start today

stat -c '%Z' (ctime) is the change-of-inode time — touch -t does not reset it without chown/chmod tricks. If your wp.integrity event timestamp matches ctime, you have a high-confidence change moment.

Step 3: Correlate with admin sessions (auth.attempt).

# Successful logins around the change time (web server log)
grep "wp-login.php" /var/log/nginx/access.log \
  | awk '$4 >= "[26/Apr/2026:03:10" && $4 <= "[26/Apr/2026:03:20"' \
  | grep " 302 "

# All POSTs to admin-ajax.php and wp-login.php in that window
grep -E "POST /(wp-login\.php|wp-admin/admin-ajax\.php)" /var/log/nginx/access.log \
  | grep "26/Apr/2026:03:1"

Each successful POST to wp-login.php returning 302 produces an auth.attempt signal with outcome=success. A login from an unfamiliar IP within ±2 minutes of the wp.integrity event is your prime suspect.

Step 4: Check the paired .htaccess integrity event.

diff /var/backups/wordpress/.htaccess /var/www/html/.htaccess
grep -nE "RewriteRule|Redirect|<If" /var/www/html/.htaccess

A second wp.integrity event for .htaccess within minutes of the wp-config.php event almost always indicates an injection chain, not an accidental edit.

Step 5: Look inside the file for the actual injection.

# Common backdoor patterns in wp-config.php
grep -nE "eval\(|base64_decode|gzinflate|str_rot13|preg_replace.*\/e|create_function" \
  /var/www/html/wp-config.php

# Anything appended after the "stop editing" line is suspicious
sed -n '/Happy publishing/,$p' /var/www/html/wp-config.php
sed -n '/stop editing/,$p' /var/www/html/wp-config.php

Each grep pattern above maps back to the wp.integrity payload: the new_hash is the diff result, these greps tell you the diff content. The signal said something changed; these commands say what.

Step 6: Walk back through PHP and FPM logs.

# PHP-FPM access log — every request that ran PHP
grep "$(date -d '2026-04-26 03:14' +%d/%b/%Y:%H:%M)" /var/log/php-fpm/www-access.log

# PHP error log around the same window — injections often error once
grep -A2 "26-Apr-2026 03:1" /var/log/php/error.log

If the injection vector was a vulnerable plugin's file-write, you will often see a single PHP warning at the same timestamp the wp.integrity event fires.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

Restoring wp-config.php is the easy part. Closing the door is the hard part. Treat every cause below as one possible entry point — work down the list until you find evidence of the actual one.

Cause 1: Compromised admin account (most common). The attacker logged into wp-admin and used a plugin's file editor or a theme upload that wrote to wp-config.php.

  • Signals produced: auth.attempt with outcome=success immediately preceding the wp.integrity event, often from a new IP/country.
  • Fix: Force-logout all sessions (wp user session destroy --all), rotate every admin password, enable 2FA, set define('DISALLOW_FILE_EDIT', true); in the freshly restored wp-config.php, and audit wp_users for accounts created within the past 30 days.

Cause 2: Vulnerable plugin with arbitrary file write. A plugin endpoint accepts a path or filename parameter without authorisation checks.

  • Signals produced: wp.integrity for wp-config.php with no preceding auth.attempt success, often paired with anomalous POSTs to admin-ajax.php or a plugin-specific REST route in the access log at the same second.
  • Fix: Identify the plugin from the request log at the change-time, disable it (wp plugin deactivate ), patch or remove. Audit all wp-content/plugins/*/ files modified the same day.

Cause 3: FTP / SFTP / hosting panel breach. The attacker bypassed WordPress entirely.

  • Signals produced: wp.integrity event with no correlated auth.attempt and no PHP request log entry at the change time. ctime changed but no HTTP traffic touched the file's directory in that minute.
  • Fix: Rotate all FTP/SFTP/hosting-panel credentials. Disable plain FTP. Enable hosting-panel 2FA. Check /var/log/auth.log and your hosting provider's audit log for the window around the change.

Cause 4: Compromised CI/CD pipeline or deployment automation. Your own tooling pushed a poisoned wp-config.php.

  • Signals produced: wp.integrity event correlated with a known deployment window, often during business hours, from a known source IP.
  • Fix: Roll the deploy key, audit the pipeline runner for unauthorised commits, verify the wp-config.php template in source control is clean.

Cause 5: Legitimate change, no incident response process. A developer edited the file in production and forgot to tell anyone.

  • Signals produced: wp.integrity event correlated with an auth.attempt success from a known team member's IP, often during business hours, with the diff being a sane constant addition.
  • Fix: Establish a "no edits in production" policy, route all wp-config.php changes through version control and deploy automation, and set up acknowledgement workflow so legitimate wp.integrity events are explicitly approved instead of ignored.

After identifying the cause: restore wp-config.php from a known-good backup or rebuild it from a template, generate fresh salts at https://api.wordpress.org/secret-key/1.1/salt/, rotate the database password, and re-hash the file to establish a new integrity baseline.

5.4 Verify

The signal you want to stop seeing is wp.integrity for wp-config.php and .htaccess outside of approved deploy windows. Healthy state is:

  • No new wp.integrity events for wp-config.php for at least 72 hours after remediation. File integrity events should be exceptional, not routine. If you are getting daily integrity events, something is still wrong.
  • Any wp.integrity events that do fire correspond 1:1 with a known deploy or admin-acknowledged change.
  • auth.attempt events show no successful logins from unrecognised IPs.
  • The shell check confirms a stable hash:
sha256sum /var/www/html/wp-config.php
# Record this. Re-run in 24h. Re-run in 72h. The hash must not change.
# No suspicious code patterns return after restore
grep -cE "eval\(|base64_decode|gzinflate" /var/www/html/wp-config.php
# Expected output: 0

If a wp.integrity event fires within those 72 hours and you cannot trace it to an authorised change, the attacker still has access — go back to section 6 and assume a different cause.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp.integrity.

The honest framing: fixing one wp-config.php compromise is not the problem. Knowing the next one happened, within minutes, is the problem. Without continuous file integrity monitoring, your detection latency for wp-config.php modifications is "whenever a visitor complains" — typically hours to days.

WordPress itself does not alert on this. Most security plugins scan inside WordPress and can be disabled by the very compromise you want to detect. Hosting-level file monitors, where they exist, surface results in dashboards nobody checks.

This type of issue surfaces as wp.integrity, which Logystera detects out-of-process and alerts on within seconds. Because the scan runs on a schedule independent of WordPress request traffic, an attacker who disables your in-WP scanner does not silence the integrity stream. Because every event is correlated with auth.attempt in the same pipeline, you do not have to manually grep two log sources at 3 a.m. to figure out which session made the change.

The detection is not magical — it is a hash comparison on a tight loop. The value is that it never gets disabled, never falls behind, and never gets lost in a noisy dashboard.

7. Related Silent Failures

Other compromises in the same wp.integrity and auth.attempt cluster, ordered by signal proximity:

  • .htaccess was modified — silent SEO-spam redirects in WordPress. A wp.integrity event for .htaccess is the single most common companion to a wp-config.php injection.
  • Unknown admin user appeared overnight — detecting wp.state_change role_escalation. When the attacker's foothold is a user-creation rather than a file edit.
  • WordPress XML-RPC brute force — diagnosing auth.attempt floods. The classic precursor to a successful admin login that ends in a wp-config.php write.
  • Plugin silently activated — tracing wp.state_change plugin_activated without an admin session. When the attacker's payload is a malicious plugin instead of a config edit.
  • wp-content/uploads is executing PHP — detecting backdoor drop with wp.integrity on new files. The other half of many wp-config.php compromises.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.