Your wp-config.php Is Fine. The Exposed File Next To It Is Not..

wp-config.php is safe because PHP executes it. Rename it .bak and the same secrets are served as plain text. Here is the probe, and the fix.

Aditya Sharma·4 min read

Short answer: a wp-config exposed to the internet is almost never wp-config.php itself. PHP executes that file and sends nothing. The problem is wp-config.php.bak, wp-config.php~, wp-config.txt and their relatives, which the web server does not recognise as code and therefore hands over as plain text, database password included.

I have been on the POSIMYTH side of enough support tickets to know how these files get there.

Nobody is careless.

Somebody was about to do something risky, took a copy first, fixed the thing, and moved on. That is good instinct. The copy is what stays behind.

Why one file is safe and its twin is not

WordPress does not protect wp-config.php by hiding it. It is protected by being executed.

The server sees .php, hands it to PHP, PHP runs it, and the constants become variables in memory. Nothing is printed. The response body is empty.

Change the extension and that entire chain stops.

.bak is not mapped to a handler, so the server falls back to what it does with any unknown file. It reads it and sends it.

Here is the same install answering both requests:

Terminal output showing two curl requests. Requesting wp-config.php returns HTTP 200 with Content-Length 0. Requesting wp-config.php.bak returns HTTP 200 with 308 bytes and the full PHP source including a DB_PASSWORD define
Run on a WordPress install I set up for this, with fabricated credentials. The

mechanism is real even though the password is not. One character of difference between the two filenames, and the second one prints the database credentials to anybody who asks.*

No exploit. No vulnerability in WordPress. Just a file being served, exactly as designed.

The filenames actually worth checking

When I look at a site I have inherited, I do not check one name. I check the family, because each one is created by a different ordinary habit.

Table of eight filenames including wp-config.php.bak, wp-config.php tilde, .wp-config.php.swp, wp-config.txt, wp-config.php.save, .env and debug.log, each marked as leaking with the human action that creates it, and wp-config.php marked safe
The middle column matters more than the first. These are not exotic attack artefacts.

They are what an editor leaves behind after a crash, or what a person creates thirty seconds before a migration they are nervous about.*

.swp is the one people dismiss. It is a binary vim swap file, so it looks like noise.

Run strings over it and the password is right there.

Check a site for an exposed wp-config in about fifteen seconds

Point this at any domain you look after:

for f in wp-config.php.bak wp-config.php~ wp-config.txt wp-config.php.save \
         wp-config.php.old .env debug.log; do
  printf '%-24s ' "$f"
  curl -s -o /dev/null -w '%{http_code}  %{size_download} bytes\n' "https://example.com/$f"
done

What you want is 403 or 404 on every line.

What should worry you is 200 with a non-zero byte count. That is the file being served.

A 200 with 0 bytes is fine, which is why the size column is in there. Status code alone will tell you the wrong thing about wp-config.php itself.

Closing it

Two moves, and the second is the one that lasts.

Configuration table showing an Apache FilesMatch block and an nginx location block that deny backup, save, swap and environment file patterns, plus a third row advising the file be removed from the web root entirely
Deny the pattern rather than the filename you happened to find. Editors keep

inventing new suffixes, and a rule listing only .bak will not catch the .save that nano writes next month.*

Then verify it. Request the file again and confirm you now get 403 rather than 200.

An unverified rule is not a fix. It is a note to yourself.

And if a file was exposed, treat the credentials as burned. Rotate the database password and the salts in wp-config.php. A deny rule added today does nothing about who read the file last March.

Why this one is worth automating

The check is trivial. That is exactly the problem.

It takes fifteen seconds, it needs doing on every site you look after, it needs doing again after every migration and every hosting move, and it produces nothing to show a client when it passes. So it gets done once, thoroughly, during onboarding, and then never again. It is the same unbilled half hour that swallows every other check on the list.

Meanwhile the file that leaks is created later, by the migration, by the crash, by the careful person taking a backup before a risky change.

This is one of the checks Protuno’s free audit runs from the domain alone, because it needs nothing but HTTP requests. Straight with you about the rest: the Super Agents that would keep re-checking it on a schedule are built but not live yet.

Until then, the loop above is yours. Run it across your portfolio once this week.

If you find one, I would rather you found it than someone else did. The same logic applies to the plugins that quietly left the directory, and to the scheduled jobs that stopped without telling anyone: silent, cheap to fix, and invisible until they are not.

You can run the free audit on a domain without installing anything.

Comments