WP-Narcan is for the morning after your WordPress site gets hacked. You point it at a copy of the site's files and it builds a clean copy next to it: fresh WordPress core, plugins and themes from wordpress.org (or from zips you supply), your uploads with anything executable taken out, and your wp-config.php once it's been checked. Anything it can't check against a clean source is set aside for you to look at instead of being copied in.
It only ever reads the site you point it at. You swap the rebuild in yourself, after you've looked at it.
Version 2.0 "Hopscotch". Free and open source under the MIT License. See CHANGELOG.md for what changed since 1.1.
- Checks it's WordPress and works out the version, language, whether it's multisite, and where wp-config.php and the content folder are (it reads wp-config.php, so a renamed
wp-contentor a wp-config.php one folder up is found by itself). - Downloads fresh WordPress core and verifies it: the zip against wordpress.org's SHA-1, then every file against wordpress.org's checksums. It also checks the old core files against the checksums for the version you had, and tells you which were changed or shouldn't be there.
- Replaces every plugin and theme with a fresh copy:
- from your own zips first, if you give it a folder of them (for paid plugins and themes);
- otherwise from wordpress.org, checked against wordpress.org's plugin checksums where they're published, and only after making sure it's the same plugin or theme (a custom plugin in a folder called
redirectionwon't be swapped for the wordpress.org Redirection plugin); - plugins wordpress.org has closed (often for security problems) are not downloaded, and are flagged, because they may be how the attacker got in. The old copies are compared with the published checksums too, so the report shows which plugin files had been tampered with.
- Sets aside what it can't verify: must-use plugins, drop-ins (
advanced-cache.php,object-cache.phpand friends), custom and child themes, plugins that aren't on wordpress.org, stray PHP files, and a wp-config.php with suspicious code in it. They go into areview/folder, with the reasons and any suspicious lines, not into the rebuild. - Copies uploads, fonts and translations and leaves out anything that could run:
.phpand other script files, hidden double extensions likephoto.php.jpg, images and icons with PHP code inside, SVGs with scripts, HTML pages with forms or scripts,.user.ini,.htaccessfiles that do more than block access, and symbolic links. It keeps the protective.htaccessfiles that WooCommerce, Wordfence and others put in uploads, so private downloads stay private. - Writes a standard WordPress
.htaccess(single site or multisite) if the old site had one, so permalinks keep working. If the old one had other rules too, it's set aside for you to compare, with anything suspicious (like redirects to other sites) pointed out. - Writes a report: a summary on screen,
report.json, a full log, andNEXT-STEPS.txtwith the checklist for after the rebuild (passwords, salts, database checks with your table prefix filled in).
- It doesn't touch your database. Rogue admin accounts, injected scripts in posts, spam links and changed settings are all in the database.
NEXT-STEPS.txthas the queries to look for them, but you have to run them and clean up. - It won't tell you how the attacker got in. The report points at what was tampered with, which helps, but finding the hole (an outdated plugin, a stolen password, another site on the same hosting) is up to you.
- It can't download paid plugins or themes. Give it the zips, or they're set aside for you to replace.
- It doesn't prove the set-aside code is safe. The suspicious-code checks are hints for a human, not a malware scanner. Code with no findings can still be malicious.
- It won't replace your live site. You do that after checking the rebuild.
- It doesn't copy caches, backups, logs or folders that aren't part of WordPress. They're listed in the report. Backups and logs on a web server can often be downloaded by anyone, so remove them from the server too.
- It only handles sites whose files live under one folder (wp-config.php may be one folder up). Layouts like Bedrock, where WordPress core and the content folder are side by side in different places, aren't supported.
You need Python 3.9 or newer on Windows, macOS or Linux, and an internet connection to reach wordpress.org.
With pip (gives you a wpnarcan command):
pip install git+https://github.com/ReignOfComputer/WP-Narcan.git
pipx install git+https://github.com/ReignOfComputer/WP-Narcan.git works too and keeps it in its own environment.
As a single file: download wpnarcan.py from this repository, install its one dependency, and run it with Python:
pip install requests
python wpnarcan.py /path/to/site
With Docker, if you'd rather not install Python. See Running it with Docker.
Try a dry run first. It checks everything and tells you what would happen, but writes nothing:
wpnarcan /path/to/site --dry-run
Then rebuild:
wpnarcan /path/to/site
It shows what it found, asks before it writes anything, and then creates two folders next to the site:
site/ your copy of the hacked site (only read, never changed)
site-rebuilt/ the clean rebuild; this is what goes back on the server
site-rebuilt-report/
NEXT-STEPS.txt what to do next, in order
report.json everything it did and found, for reading or for scripts
wpnarcan.log the full log
review/ everything that was set aside, laid out like the site
Nothing is written inside site-rebuilt/ except the site itself, so no logs or lists end up on your web server.
Read the summary, then work through NEXT-STEPS.txt. If you move something from review/ into the rebuild, put it at the same path (for example review/wp-content/themes/my-child-theme goes to site-rebuilt/wp-content/themes/my-child-theme).
WP-Narcan runs on your own computer, so you only need a copy of the files.
- Download the whole site folder. With cPanel, open File Manager, select the site folder (often
public_html), use Compress to make a zip, download it and unzip it on your computer. With FTP (FileZilla, for example), turn on hidden files first (Server > Force showing hidden files) so.htaccessand.user.inicome too, then download the folder. If your wp-config.php is one folder abovepublic_html, download it as well and put it next to the folder the same way. - Run WP-Narcan on the copy:
wpnarcan public_html - Check the report and deal with anything in
review/. - Upload
public_html-rebuiltnext to the live folder, then rename the live folder to something likepublic_html-hackedand the rebuild topublic_html. In cPanel, File Manager can do both (upload a zip and Extract is much faster than uploading lots of small files). Don't upload the rebuild over the top of the old files: that leaves the attacker's extra files in place. - Keep the old folder off the web until you're done with it, then delete it.
You can run WP-Narcan on the server itself if it has Python 3.9+, or copy the site to your computer with rsync or tar first. On the server:
python3 wpnarcan.py /var/www/example.com
Run it as the user that owns the site so the rebuilt files have the right owner, or fix ownership afterwards (for example chown -R www-data:www-data /var/www/example.com-rebuilt).
Put the zips you downloaded from the vendor into a folder and point WP-Narcan at it:
wpnarcan /path/to/site --local-repo-plugins /path/to/plugin-zips --local-repo-themes /path/to/theme-zips
A zip is used for a plugin if it's named after the plugin's folder (my-premium-plugin.zip for wp-content/plugins/my-premium-plugin) or if the folder inside it has that name (so my-premium-plugin-2.3.1.zip containing my-premium-plugin/ works too). The plugin is always installed under the folder name the site used, so WordPress still finds it. Your zips are tried before wordpress.org. Marketplaces sometimes give you a "full package" zip with documentation and the real plugin zip inside; use the inner, installable zip.
--yes skips the question, and --json prints the whole report as JSON on standard output (the human-readable messages go to standard error). It also never prompts when there's no terminal. Check the exit status:
| Exit status | Meaning |
|---|---|
| 0 | Done, and nothing needs your attention. |
| 1 | Done, but something needs your review (set-aside items, tampered files, things left out of uploads). |
| 2 | Nothing was done: bad arguments, not a WordPress folder, the output already exists, or you answered no. |
| 3 | The rebuild failed (for example WordPress couldn't be downloaded or didn't match its checksums). Nothing was written to the output folder. |
| 130 | Interrupted. |
--dry-run --json makes a good read-only check: exit status 1 means something needs a look.
WP-Narcan won't overwrite an existing rebuild. Use --force to replace one it made earlier (it checks the report folder to be sure it's its own). The new rebuild is built in a temporary folder and only swapped in once it's complete, so a failed or interrupted run leaves the previous rebuild alone and never leaves a half-finished one behind.
By default WP-Narcan installs the latest WordPress, plugins and themes, because an outdated plugin is the most common way in. If the latest versions don't work on your server (an old PHP version, say), use --keep-versions to reinstall the versions the site had. The summary tells you the newest PHP version the rebuild needs.
--new-salts gives the rebuilt wp-config.php new authentication keys and salts, which logs everyone out and stops stolen login cookies working. A few plugins (Site Kit by Google, for one) use the salts to encrypt stored data and will need reconnecting afterwards.
These are found automatically from wp-config.php. If WP-Narcan can't work out where the content folder is (for example it's set from an environment variable), tell it with --wp-content-dir, using the folder's name or its full path inside the site. A wp-config.php that lives above the site folder isn't part of the rebuild; it stays where it is, and a copy is put in the report folder for reference.
Build the image once from this repository:
docker build -t wpnarcan .
Then run it with the folder that contains your site mounted at /work. On macOS and Linux:
docker run --rm -it -v "$PWD:/work" wpnarcan /work/public_html --dry-run
docker run --rm -it -v "$PWD:/work" --user "$(id -u):$(id -g)" wpnarcan /work/public_html
--user makes the rebuilt files yours instead of root's. In Windows PowerShell:
docker run --rm -it -v "${PWD}:/work" wpnarcan /work/public_html
Leave out -it in scripts; WP-Narcan won't ask anything without a terminal.
wpnarcan SITE [options]
-o, --output DIR where to write the rebuild (default: next to the site, named SITE-rebuilt).
The report goes next to it in DIR-report
--dry-run check the site and report what would happen without writing anything next to
it (plugins and themes are still downloaded to a temporary folder to check them)
--json print the report as JSON on stdout (messages go to stderr). Implies --yes
-y, --yes don't ask before starting
--force replace an earlier WP-Narcan rebuild at the output location
--keep-versions reinstall the versions the site had instead of the latest ones
--local-repo-plugins DIR folder of plugin zips to use before wordpress.org
--local-repo-themes DIR the same, for themes
--wp-content-dir PATH the content folder, if it can't be found from wp-config.php
--new-salts give the rebuilt wp-config.php new keys and salts (logs everyone out)
-v, --verbose show debugging detail
--version show the version
NEXT-STEPS.txt is written for your site, but the short version is:
- Review everything in
review/. Only move things into the rebuild if you know what they are and they're clean. - Back up the live site (files and database) and keep the backup off the server.
- Swap the folders rather than uploading over the old files.
- Change every password: WordPress administrators, the database user, hosting, FTP/SFTP/SSH and email on the same hosting.
- Replace the keys and salts in wp-config.php.
- Check the database for administrator accounts you don't recognise, application passwords, injected scripts in posts and options, and changed settings.
- Re-save your permalinks and your caching plugin's settings, then update everything and remove what you don't use.
- Look for the way in: access logs, other sites on the same hosting, scheduled tasks, PHP settings outside WordPress.
The tests need either Python or Docker.
Unit tests run offline against a small stand-in for wordpress.org:
pip install -e ".[test]"
pytest
Or in Docker, with nothing installed locally:
docker compose -f tests/compose.yaml run --rm --build unit
Set PYTHON_VERSION=3.9 (or another version) in the environment to test with a different Python.
The end-to-end test builds two real WordPress sites in Docker (MariaDB, the official WordPress and wp-cli images) and downloads from wordpress.org, so it needs an internet connection and takes a few minutes:
docker compose -f tests/compose.yaml run --rm --build e2e
docker compose -f tests/compose.yaml down -v
The second command removes the containers, network and volumes the test created. With make installed, make test, make e2e (which cleans up after itself) and make clean do the same.
What the end-to-end test does:
- Site A is WordPress 7.0.6 in German with real wordpress.org plugins and themes, a child theme, a "paid" plugin supplied as a zip, a custom plugin whose folder name matches a different wordpress.org plugin, and a plugin wordpress.org has closed. It then gets planted with harmless test infections: indicator strings and file shapes that WP-Narcan should catch, such as
eval(base64_decode('ZWNobyAidGVzdCI7'))(which decodes toecho "test";) insideif ( false ), a web-shell-shaped file in uploads, PHP hidden in an icon, a tampered core file, a rogue must-use plugin, a modified wp-config.php, a redirect in.htaccess, and symbolic links pointing out of the site. None of it can do anything. - Site B is a current multisite network with a renamed content folder and wp-config.php above the web root.
- WP-Narcan runs from its Docker image: a dry run, a rebuild, a second run without
--force(which must refuse), and a--forcerun. - The test checks that the old sites weren't changed at all, that no indicator reached the rebuild, that everything suspicious was set aside rather than lost, and that the report is right. wp-cli then verifies the rebuilt core and plugins against wordpress.org's checksums, and both rebuilds are served by Apache: pages, pretty permalinks, the child theme, the paid plugin, uploaded images and multisite subsites all have to work, and a protected download has to stay protected.
If you'd rather someone else cleaned it up, database included, BackHAUS does that: backhaus.sg/services. Bugs and suggestions for WP-Narcan are welcome as GitHub issues.
WP-Narcan is licensed under the MIT License. See LICENSE.
If WP-Narcan helped you, you can support the project with Bitcoin:
BTC: bc1q76vc0emvwv9xkv34mydfaa9lme2unc9g07su9x
