There is no clean copy of this file. Cut the injection out by hand.
Custom theme code, a plugin built for one site, and wp-config.php have no pristine version anywhere. Every guide that says replace it from the original is talking about a different file. Here is the manual route, and the checks that tell you the file still does what it did before.
Checked against a live WordPress install on
.
Why no scanner can repair this file
Look for the Repair button once, then stop looking. In Wordfence 9.0.0
that control is gated on the file matching a WordPress.org release, so
for a file it cannot match it never renders at all. Sucuri Security 2.7.4
restores WordPress core and nothing else; plugin files, theme files and
wp-config.php sit outside its integrity check. Malcure Malware Shield
19.9.8 refuses to delete wp-config.php. The same gate runs through all of
them: a file gets a repair path when it can be recognised as WordPress
core, a WordPress.org plugin, or a WordPress.org theme. Your code matches
none of the three.
Command-line checks stop at the same wall. There is no theme checksum
subcommand in WP-CLI, and WordPress.org publishes no theme checksum API
to back one.
bash
$ wp theme verify-checksums
Error: 'verify-checksums' is not a registered subcommand of 'theme'.
The plugin command exists and walks straight past anything it cannot
resolve upstream. It reports success while doing so, so the exit code
says nothing useful about a plugin you wrote.
bash
$ wp plugin verify-checksums --all
Warning: Could not retrieve the checksums for version 1.0.0 of plugin acme-tools, skipping.
Success: Verified 0 of 1 plugins (1 skipped).
A child theme has no upstream even in principle. One was measured for
this cluster and it held a single file, style.css, with a
Template: header pointing at its parent; every other
template resolved out of the parent's directory. Reinstalling the parent
from WordPress.org never touches the child.
If the flagged file is core, a WordPress.org plugin or a WordPress.org
theme, close this page and use
restore modified core, plugin and theme files
instead. Fetching the published copy is faster and safer than any edit
you can make by hand. The rest of this page is for the files that route
cannot reach.
Copy the file before you touch it
You get one shot at this file. Take the copy first, and make the edits
somewhere the site is not reading.
Copy the file off the server. Your own machine is the
right place. If you have to keep the copy on the server, keep it
outside the web root, in your home directory rather than anywhere
under the WordPress folder.
Record what you started with. Run
sha256sum against the file before you change a byte and
keep the output with your notes. When someone asks in a week which
version you edited, that hash is the answer.
Do not leave a copy inside the site. A file named
functions.php.bak in the theme folder is a file your
server may hand to anyone who guesses the name, and for wp-config.php
that means your database password. Scanners treat those leftovers as
findings in their own right: Wordfence 9.0.0 collects abandoned
.suspected files and raises an issue for each one your
server serves over HTTP.
Edit the copy. Put it back only after it passes the
checks in the last section of this page.
Use an editor that does not reformat on save. A reformat turns a
four-line removal into a diff of the whole file, and the diff is the
main thing telling you whether you cut what you meant to cut.
Where the injection sits
Injected code goes into one of four places, and each one has its own
tell. The blocks below are skeletons. The payload is replaced by a
comment in every one of them, so what you are reading is the shape and
the position, never working code. Yours will look denser and longer than
these, and it will sit in exactly one of these four spots.
1. At the top, above the file's own comment
php
<?php
/* SKELETON. Payload removed. The real thing is usually one long line. */
if ( ! function_exists( 'wp_theme_cache_init' ) ) {
/* decoder and payload removed from this example */
}
/**
* Acme theme functions. <- your file starts here
*/
This is the most common shape because it is the cheapest to write: open
the file, seek to zero, write. It runs on every request that loads the
file, before anything of yours. The invented function name usually
borrows WordPress vocabulary so it reads as native at a glance. Check
line one through to your file's own opening comment or its first
function.
2. At the bottom, after your last line
php
add_action( 'after_setup_theme', 'acme_setup' );
/* your file used to end here */
/* SKELETON. Payload removed. Appended block, often after blank lines. */
function wp_admin_session_check() {
/* payload removed from this example */
}
wp_admin_session_check();
Scroll to the end. Whatever sits after your last statement is the
candidate, and a run of blank lines above it is a common giveaway,
because the writer appended rather than inserted. An appended block that
defines a function has to call it too, so look for the call as well as
the definition.
3. Inside a function that was already there
php
function acme_get_header_scripts() {
/* SKELETON. Your body was here. The injection replaced it. */
/* payload removed from this example */
}
The hardest of the four to see. The file length looks normal, nothing
sits at either end, and the function is still called from all the places
it was called from before. Nothing about the file's structure changed,
so only behaviour gives it away. Compare the function against your
version control, a staging copy, or another site running the same code.
If the scanner named this file and the top and bottom are clean, this is
where to look next.
4. One added line that pulls in a second file
php
require_once __DIR__ . '/inc/.cache-config.php';
One line, in a file full of lines that look like it. The payload lives
in the file it names, and a filename that starts with a dot keeps that
file out of a plain ls, so use ls -la on the
directory. Both halves have to go. Cutting the line leaves the second
file sitting there for the next thing that finds it, and deleting the
second file while leaving the line gives you a fatal error on the next
request.
Finding it faster
Three of the four shapes announce themselves by line length, because a
script wrote them and a script does not press return. This prints every
line over 500 characters with its number and its length:
A long line is a lead, not a verdict. A minified asset is long on every
line, from the first to the last, because the whole file was generated
in one piece. An injection is one long line in a file where everything
else is a reasonable length.
Telling a real detection from a false flag
goes through that distinction and what to do when you still cannot call
it.
Read it, then cut it out
Spend two minutes reading before you delete. You are about to destroy
the only record of what was done to the site, and what the block reaches
for tells you what else to check.
It fetches a remote address and runs the response.
Whoever wrote it can change the payload without touching your site
again. Treat every writable file as suspect, not this one alone.
It writes a file. Note the path it writes to. Removing
this block does nothing about the file it already created, and that
file is the one that survives your cleanup.
It reads a request parameter, a cookie or a header and acts on
it. That is a door, and it was reachable by anyone who knew
the file existed. Your server's access log for that path is where you
find out whether anyone used it.
It redirects on a condition. Visitors and search
engines saw something you never did. Check whether the site got
flagged while it was running, and check it from a search result rather
than by typing the address.
Read the code. Do not run it to find out what it does, and do not paste
it into an online decoder that executes it for you.
Where the boundaries are
An injection is a guest in your file. It brings its own braces, defines
its own names, and touches none of your variables. That self-containment
is what makes it removable at all.
Start at its first character and end at its own closing brace or
semicolon. If it defined a function and then called it, the call goes
with the definition. If you cannot see where the block ends, count braces
from its opening line: when the count returns to zero you are at the last
character to remove. Everything your own code needs was there before the
block arrived, so nothing you keep should refer to anything you cut.
The tag traps
Four ways a deletion that looks right breaks the file. The syntax check
catches two of them and stays silent about the other two, which is why
the last section has more than one step in it.
You cut the file's opening tag. A block glued to the
top of the file starts on the same line as
<?php often enough that selecting the block takes the
tag with it. The file then has no PHP in it, so PHP prints it. I
checked this: a file with the tags removed passed the syntax check
cleanly and printed its whole contents as text.
You left a second opening tag behind. Some blocks close
PHP and reopen it. Cut the middle and leave the reopening tag and the
parser stops at it.
bash
Parse error: syntax error, unexpected token "<", expecting end of file in functions.php on line 3
Errors parsing functions.php
You took one brace too many. The message names two line
numbers and the first one is the useful one. It points at the brace
that never closed. The second is the end of the file, which is where
PHP gave up.
bash
Parse error: Unclosed '{' on line 2 in functions.php on line 4
Errors parsing functions.php
You left a fragment after the closing tag. Anything
after ?> is output rather than code, so the syntax check
has no opinion about it and PHP prints it to the browser. Same for a
blank line above the opening tag. Look at the first and last bytes of
the file with your own eyes.
wp-config.php, specifically
This file has the fewest tools pointed at it of anything in your
installation. It appears in no checksum set. Neither WordPress release
archive contains it: the full 7.0.4 zip holds 4,401 files and the
no-content zip 3,901, and what both ship is
wp-config-sample.php. WP-CLI's core checksum command
excludes it by name in its filter. So nothing will tell you the file was
modified, and nothing can put it back. Several scanners special-case it
for the same reason and refuse to remove it at all.
Find it before you edit it, because it does not have to be in the
docroot. On one install checked for this page it sat a level above.
bash
$ wp config path
/var/www/html/wp-config.php
What belongs in the file
The database constants.DB_NAME,
DB_USER, DB_PASSWORD, DB_HOST,
and usually DB_CHARSET and DB_COLLATE.
The eight keys and salts.AUTH_KEY,
SECURE_AUTH_KEY, LOGGED_IN_KEY,
NONCE_KEY and the four matching _SALT
constants. Long random strings, and nothing else.
The table prefix. One line setting
$table_prefix.
Debug constants, if you set them.WP_DEBUG, WP_DEBUG_LOG,
WP_DEBUG_DISPLAY.
The definition of ABSPATH, then the require of
wp-settings.php. Those are the last two things in
the stock file.
Constants your host or a plugin asked for.WP_HOME and WP_SITEURL are the usual ones.
The stock file marks its own boundary. It carries a comment saying to add
custom values between that line and the stop editing line, then a line
reading that this is all and you should stop editing, and below that only
the ABSPATH definition and the require. Anything of yours
belongs above the stop editing line. Anything you find below it was put
there by someone, which is a reason to read it rather than proof it is
hostile: hosts append constants there too.
The other rule is about verbs. This file defines values. Nothing in it
should fetch a remote address, decode a string into code, open a socket,
write a file, or include something from a directory you do not
recognise. A line doing any of that does not belong regardless of how
official its name looks.
Listing it without printing your credentials
WP-CLI can show you every name in the file with none of the values, which
makes it safe to look at over a shared screen and fast to scan for
something that should not be there.
bash
$ wp config list --fields=name,type
name type
table_prefix variable
DB_NAME constant
DB_USER constant
DB_PASSWORD constant
DB_HOST constant
AUTH_KEY constant
SECURE_AUTH_KEY constant
WP_DEBUG constant
wp-config-ddev.php includes
Trimmed. A real install lists all eight keys and salts and whatever else
it carries.
The row of type includes is the one to read twice. It names
a file that wp-config.php pulls in, which is the fourth shape from the
section above showing up in the one file where it is easiest to miss. A
hosting stack legitimately puts a file there. A path you cannot account
for is worth opening.
When the file is clean, treat the credentials as spent. Whoever edited it
read your database password and your keys. Change the database password
with your host, update DB_PASSWORD to match, and replace all
eight keys and salts with fresh random strings.
Prove the file still works
A file with no upstream copy cannot be verified by comparison with the
publisher, so you verify it by parts: it parses, it differs from the
original only where you cut, and it still does its job.
Syntax check it.php -l reads the file
and parses it without running any of it.
bash
$ php -l functions.php
No syntax errors detected in functions.php
Anything else is a parse error naming the file and a line. Fix it
before the file goes anywhere near the site.
Diff it against the copy you took. Read every line of
the output, not the count. The only lines that should appear are the
ones you meant to remove.
bash
$ diff -u /home/you/functions.php.orig wp-content/themes/acme/functions.php
--- /home/you/functions.php.orig 2026-08-18 16:39:36.295351885 +0200
+++ wp-content/themes/acme/functions.php 2026-08-18 16:39:36.297351906 +0200
@@ -1,6 +1,4 @@
<?php
-/* the block you cut shows up here, prefixed with a minus */
-/* and nothing else should */
/**
* Acme theme functions.
*/
A line you did not intend means your editor reformatted the file. Start
again from the copy.
Diff it against your own history. If the theme or the
plugin is in version control, the comparison against your last known
good commit is the strongest check available for this file, because it
compares your code against your code. A staging copy of the same site
is the next best thing.
Exercise the feature. A parse is not a behaviour. Load
the page that uses the function you edited, then load the admin, then
submit whatever form the file was responsible for. A file that parses
and returns the wrong thing looks fine from the command line.
Go after what the block left behind. If it wrote a
file, that file is still there. If it pulled in a second file, that one
is still there too. Removing the caller is half the job, and the half
that gets remembered.
The syntax check is narrower than it looks. On a test file with a stray
fragment sitting after the closing tag it reported no syntax errors, and
running the file printed the fragment. Loading the page in a browser is
the step that catches that class of mistake.
Do not expect the re-scan to confirm your work the way it would for a
core file. The commands that clear core have no equivalent here, and a
checksum command that skips your plugin still exits successfully. Silence
from those tools means nobody checked.
When there is nothing to salvage
Some files have no injection to cut out, because the file is the
injection. Three signs, any one of which is enough:
It is a PHP file in your uploads directory. Uploads
hold media. Code that arrived there arrived by a route you did not
intend.
The whole file is one dense block with no readable code around
it. There is no host file to preserve, because there was never
a host file. Compare that against a legitimate packed library, which
ships inside a plugin or theme directory and appears in that
component's published archive.
Nothing references it. Search the theme and the plugin
for the filename. If nothing includes it, nothing requires it, and you
did not write it, it is not part of your site.
For those, the file goes. Write the path down before you remove it,
because the same path coming back is how you learn the way in is still
open.
The harder case is a file where you can see the injection and you cannot
see where your own code ends. Stop cutting at that point. Guessing at the
boundary leaves you with a file that parses, runs, and is subtly wrong,
which costs more to find later than the hour you saved. Three routes, in
the order they are worth trying:
Your version control. Check out the file from the last
commit you trust and compare it against what is on the server.
A staging or development copy. Confirm its date first.
A staging site cloned after the injection has the injection.
Your host's backup, restored beside the site rather than over
it. Restore into a separate directory so you compare files
instead of overwriting a site you have already partly cleaned.
Pick the backup by date rather than by the file's timestamp. The
modification time is set by whatever wrote the file, so it can say
anything. If none of the three routes exists, restoring the whole file
from before the injection beats a half-confident edit, and rebuilding the
one function you lose is a smaller job than debugging a file you cut by
guesswork.
Questions
Can I replace an infected wp-config.php with wp-config-sample.php?
No. The sample file carries placeholder text where your database name, user, password and host belong, and it carries no keys or salts. Copy it over your file and the site stops connecting to its database. What the sample is good for is structure: open it beside your file and use it to see which lines in yours are stock and which were added. Neither WordPress release archive contains a wp-config.php at all, only wp-config-sample.php, so the sample is the closest thing to a reference copy that exists.
How do I check a custom theme file against a clean copy?
There is no clean copy to check against, and there is no command that pretends otherwise. WP-CLI has no theme checksum subcommand: `wp theme verify-checksums` answers that verify-checksums is not a registered subcommand of theme. WordPress.org publishes no theme checksum API either. Your comparison points are your own version control, a staging copy of the same site, or another site running the same code. For a child theme there is nothing upstream even in principle: a child theme is your files plus a Template header, and reinstalling the parent never touches it.
Does wp plugin verify-checksums work on a plugin I wrote myself?
It runs and it tells you nothing. A plugin with no WordPress.org manifest is skipped, and the command still reports success and exits 0. On a single custom plugin the output reads: Success: Verified 0 of 1 plugins (1 skipped), with a 404 warning above it for the checksum file it could not fetch. Read the warnings rather than the exit code, and treat a skipped plugin as unchecked rather than clean.
The file passes php -l but the page shows stray characters at the top.
You left a fragment after the closing PHP tag. Anything after ?> is output, not code, so the syntax check has no opinion about it and PHP prints it. I checked this on a test file: php -l reported no syntax errors and running the file printed the leftover characters. The same applies to a blank line above the opening tag. Open the file, look at the very first and very last bytes, and remove anything outside the tags that was not there before.
Should I just delete the whole file instead of editing it?
It depends on what the file is. A PHP file that consists of nothing but the injection has nothing worth keeping, so removing it costs you nothing. A theme functions.php or a plugin's main file is a different matter, because the site loads it on every request. Deleting a required plugin file gives a fatal error and a critical error page. Emptying the same file gives no error at all, the page still renders, and the feature just stops working, which is harder to notice and harder to trace.
This file is the case that shaped how Segurium cleans. An injection in a
legitimate file gets the injected code removed and the rest of the file
written back in place, so your theme keeps its functions and your plugin
keeps loading. A file that is nothing but malware becomes zero bytes at
the same path instead of disappearing, because a deleted file that
something still includes takes the site down while an empty one does not.
When no clean version of a file can be produced, the cleanup stops and
leaves the file exactly as it found it. The original goes into an
encrypted backup on your own server before any of that happens.