Guide
1. Problem
Your backup tool, your version control diff, or a file manager's "modified" column tells you wp-config.php changed two days ago. Nobody on the team remembers editing it. There was no deploy that week. The site looks fine: pages load, checkout works, no support tickets. But the file holding your database credentials and authentication salts is not the file it was last Tuesday, and you have no idea why.
If you searched "wp-config.php changed who edited it" or "wordpress config file modified security", most answers stop at "restore your backup" or "run a malware scanner." That skips the question that matters: was this a deploy you forgot, an operator fumbling a late-night fix, or someone who should not have write access to your server. Restoring a backup before answering that can throw away the only evidence you have.
The real question is narrower: what changed, when, and does the timing line up with something you did on purpose. A modification to wp-config.php surfaces as the wp.integrity signal, the same file-drift detector that watches core, plugin, and theme files, applied to the one file WordPress trusts unconditionally.
This guide covers diagnosing what changed and when, separating the three explanations (deploy, mistake, compromise), and fixing whichever one you land on. For the general mechanics of file-drift detection, see the file integrity monitoring guide; this one is specific to the file that matters most.
2. Impact
wp-config.php is not just another PHP file. It is the one place WordPress keeps the credentials and switches that determine how much damage a bad actor, or a bad edit, can do.
- Database credentials in plain text.
DB_NAME,DB_USER, andDB_PASSWORDlive in this file, unencrypted, on the filesystem. Anyone who can read it can connect to your database directly, bypassing WordPress entirely, including its logging. - Authentication salts, if stolen, forge sessions. The eight
AUTH_KEY/SECURE_AUTH_SALTconstants sign every login cookie WordPress issues. An attacker who copies them can mint a valid admin session without ever touching/wp-login.php, so there is no failed-login trail to find later. - Behavioral toggles change what the site does, silently. Flipping
WP_DEBUG_DISPLAYon leaks file paths and query fragments to every visitor. SettingDISABLE_WP_CRONtotruequietly stops scheduled posts and housekeeping with no error anywhere. Neither crashes the site, which is why nobody notices for weeks. - It is the most common place to hide persistence. A single appended
requireoreval(base64_decode(...))line at the bottom runs on every request, before any plugin loads, and survives a full plugin and theme wipe. Cleaning up every plugin does nothing if this file still has the backdoor.
Because the file rarely changes on a healthy site, and the cost of missing a bad change is high, a modification here deserves a faster, more deliberate response than almost anything else on the filesystem.
3. Why It’s Hard to Spot
WordPress has no concept of "this file should not change." There is no admin screen showing the current contents of wp-config.php, no hash stored anywhere WordPress checks against, and no hook fired when the file is edited outside WordPress itself. If someone opens it in a file manager, over SFTP, or through a compromised shell and saves a change, WordPress has no way to know.
The tooling that does exist mostly ignores this file on purpose. wp core verify-checksums only covers files under wp-admin and wp-includes; wp-config.php is not part of a release checksum manifest, since every install is supposed to have a unique one. Security plugins that offer file monitoring often exclude it by default, or flag it constantly and get ignored, because managed hosts rewrite parts of it (memory limits, cache settings) on every deploy, training you to treat the alert as noise.
Version control does not help either unless you specifically committed this file, and most teams do not, since it holds live credentials. The file with the highest blast radius on the whole site is usually the one file nobody is diffing, and its modification time alone tells you nothing: not who touched it, what changed, or whether the new content is benign.
4. Cause
The wp.integrity signal fires when the SHA256 of a tracked file, including wp-config.php, no longer matches the known-good baseline. For this file, drift resolves into one of three explanations, each leaving a different trace in the surrounding signals.
- A legitimate deploy or config edit. A migration to a new database host, a plugin installer that adds a required constant, or an intentional change to memory limits or cache settings. This usually lands close in time to a matching entry in your deploy log, and often coincides with
wp_state_changes_totalfor the plugin whose installer wrote the constant. - An operator mistake. Someone raised the memory limit while chasing an unrelated fatal, flipped
WP_DEBUG_DISPLAYon to troubleshoot and forgot to revert it, or fat-fingeredDB_HOSTduring a host migration. If it happened alongside a host-level change, you may see a matchingwp_environment_changes_totalentry (a PHP version bump, a plugin count change) in the same window. Often there is no correlate at all, which is itself the tell that this was a manual, unlogged edit. - A compromise. Malware that gained write access, through a stolen admin session, a plugin vulnerability, or a compromised hosting credential, very commonly targets this exact file. The two dominant patterns are an appended
requirepulling in a remote or hidden payload, and aneval(base64_decode(...))block dropped past the "stop editing" comment. This drift usually has no matching deploy entry and no matchingwp_state_changes_total, and is often preceded by a successfulauth.attemptfrom an unfamiliar IP in the hours before.
The signal only tells you a hash changed. Which explanation applies is answered by reading the diff and lining the timestamp up against everything else happening at the same moment.
5. Solution
5.1 Diagnose (logs first)
Do not restore from backup yet. Read the diff first. Once you overwrite the file, you lose the evidence of what was in it.
1. Confirm the drift and get its timestamp. If you are running the file-integrity check described in the companion guide, the modification shows up as a wp.integrity event with a file path and an mtime.
grep "wp-integrity" /var/log/syslog | grep "wp-config.php"
# Or, if you are not yet running scheduled checks, get the mtime directly
stat -c '%y %n' /var/www/html/wp-config.php
A typical drift line looks like this:
[13-Jul-2026 02:47:03 UTC] wp-integrity: wp-config.php hash changed: baseline=a1f9c3...e2 current=7b04de...91 mtime=2026-07-13T02:46:58Z
2. Diff against a known-good copy. You need a copy from before the change: a filesystem backup, a snapshot, or the baseline the integrity check stored outside the web root. Diff line by line rather than skimming.
diff /var/backups/wp-config.php.known-good /var/www/html/wp-config.php
3. Look past the "stop editing" line. WordPress marks the boundary with a comment: everything below is bootstrap code, and legitimate edits almost never touch it.
/* That's all, stop editing! Happy publishing. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
// Anything appended after this block, especially require, include,
// eval(), or base64_decode(), is the single highest-signal thing to check.
require_once ABSPATH . 'wp-settings.php';
A legitimate constant addition (a new define() line, an updated DB host) belongs above that comment. Anything appended below require_once ABSPATH . 'wp-settings.php';, or inserted between the closing ?> tag (if one exists) and end of file, is not something a normal config change would produce.
4. Cross-check the timestamp against your deploy log and the surrounding signals. Line the mtime up against anything you can find that explains it.
# Does a deploy entry match the mtime within a few minutes?
grep "2026-07-13" /var/log/deploys.log
# Did a plugin state change or environment change land in the same window?
grep -E "(wp_state_changes_total|plugin_updated|plugin_installed)" /var/log/syslog \
| grep "2026-07-13"
If wp_state_changes_total shows a plugin install or update in the same minute, that installer likely wrote the constant you are looking at. If wp_environment_changes_total shows a PHP or WordPress core version change in the same window, check wp_environment_info for the before/after values; a host-level PHP or core upgrade can explain a rewritten file. If neither lines up, and there is no deploy log entry either, you are looking at an edit nobody logged.
5.2 Root Causes
Map what the diff shows to the likely explanation before deciding how to respond.
- A new
define()line above the "stop editing" comment, added minutes after a plugin install, is a plugin installer writing its required constant. Cause: legitimate deploy. Signal:wp.integritypaired withwp_state_changes_totalfor the same plugin. - Changed
DB_HOST,WP_MEMORY_LIMIT, or a toggledWP_DEBUG_DISPLAYvalue, with no matching deploy log entry, usually means a manual, unrecorded edit. Cause: operator mistake. Signal:wp.integritywith nowp_state_changes_totalorwp_environment_changes_totalcorrelate. - Anything appended past
require_once ABSPATH . 'wp-settings.php';, especially arequireoreval(base64_decode(...)), is almost never legitimate. Cause: compromise. Signal:wp.integritywith no legitimate correlate, often preceded within hours by a successfulauth.attemptfrom an unusual source. - Changed authentication salt values with no security-rotation ticket behind them is a specific, serious version of the same pattern: an attacker read the old salts to forge a session, then rewrote them to lock admins out of any prior session. Cause: active compromise. Signal:
wp.integrityon the salt block specifically.
5.3 Fix
The fix depends on which explanation the diff supports. Do not skip straight to "restore and rotate everything" if the change was explained; that just trains everyone to ignore the next alert.
Verify the diff, line by line, before you decide anything. Every changed line needs an explanation you can point to: a deploy log entry, a plugin installer, a person who admits to the edit. A line you cannot explain is treated as unexplained, not as probably fine.
If it is explained, confirm and re-baseline. Capture the new file as the baseline so the next scan does not re-flag the same, now-legitimate, content.
sha256sum /var/www/html/wp-config.php > /var/log/wp-integrity/wp-config-baseline.sha256
If it is unexplained, treat it as a compromise. A wp-config.php change you cannot account for is not proof of an intrusion, but it is a finding that should stop you from moving on without checking further. Do the following in order:
# 1. Restore the known-good copy (do this after you've saved the bad
# version elsewhere for analysis, not before)
cp /var/www/html/wp-config.php /var/quarantine/wp-config.php.suspect
cp /var/backups/wp-config.php.known-good /var/www/html/wp-config.php
# 2. Generate fresh authentication salts
curl -s https://api.wordpress.org/secret-key/1.1/salt/
# 3. Rotate the database password and update DB_PASSWORD to match
# (do this at the database server, not just in the file)
wp config set DB_PASSWORD '' --type=constant
Then hunt for the entry point. Restoring the file without finding how the attacker wrote to it means they can do it again. Check for the successful auth.attempt that preceded the change, look for an outdated plugin with a known file-write vulnerability, and check for other wp.integrity events around the same time that might point to a webshell elsewhere on the filesystem.
5.4 Verify
The fix is confirmed when the file stops drifting, not when it loads without a visible error. A restored file with an open entry point will simply be rewritten again.
# Re-hash and confirm it now matches your new baseline
sha256sum /var/www/html/wp-config.php
cat /var/log/wp-integrity/wp-config-baseline.sha256
# Watch for any further drift over the next full check cycle
grep "wp-integrity" /var/log/syslog | grep "wp-config.php" | grep "$(date '+%b %_d')"
A clean outcome is zero further wp.integrity events on this file for a full scan cycle after the fix, with the password and salt rotation confirmed by a successful login using the new credentials. If unexplained drift reappears within hours or days, the entry point was not actually closed and the restore only bought time, not resolution.
6. How to Catch This Early
By the time you notice a stale backup diff or a suspicious file manager timestamp, the change could be weeks old. The gap between the file changing and you finding out is the entire problem.
This issue surfaces as wp.integrity.
Nothing in default WordPress alerts on this specific file changing. There is no admin notice, no email, no entry in any built-in log. WordPress trusts wp-config.php unconditionally on every request and has no mechanism to check whether it is the same file that ran an hour ago. The only thing that changes when this file is modified is behavior, a new database connection, a different debug setting, a payload that now runs on every page load, and none of that produces a WordPress-native alert.
Reading the file's hash from outside WordPress on a schedule, and counting a hash mismatch as a signal instead of something you'd discover by accident, is what closes that gap. A drift on this file, correlated against your deploy log and the plugin and environment signals around the same time, turns "I noticed the timestamp was wrong three weeks later" into "this file changed at 02:47 and nothing else explains it," which is the difference between catching a compromise while it is fresh and catching it after the credentials it exposed have already been used elsewhere.
7. Related Silent Failures
- wp.integrity drift elsewhere on the filesystem: a wp-config.php change rarely happens in isolation. Check for the same drift on core files, theme functions.php, or new PHP files under uploads. Covered in the file integrity monitoring guide.
- wp_environment_changes_total drift with no deploy behind it: a PHP version, core version, or multisite flag changing outside a recognized window. See the environment drift guide.
- wp_state_changes_total for a plugin nobody remembers installing: a common way attackers add persistence that looks like ordinary plugin activity, not a filesystem edit.
- A new administrator account appearing around the same time: the next step an attacker with stolen database access or a forged session usually takes. See the admin user added guide.
- A successful auth.attempt from an unfamiliar IP just before the drift: the credential event that most often precedes an unexplained wp-config.php change. Treat the pair as one incident, not two findings.
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.