A plugin ZIP reaches you in many ways: a client sends it by email, you find it in an old backup, you download it from a marketplace or a GPL redistributor, a freelancer leaves it in a shared folder. Once you click Upload Plugin, every PHP file in that archive runs with full access to the site and its database.
Here is the routine I use before installing any archive that did not come straight from wordpress.org. It takes about ten minutes and only needs a terminal.
All examples use Linux/macOS. On Windows, run them in WSL.
1. Compare the checksum
If the source publishes a SHA-256 checksum, check it first. It tells you the file you have is exactly the file they published — not truncated, not re-packed, not swapped on the way.
sha256sum plugin.zip # Linux
shasum -a 256 plugin.zip # macOS
Get-FileHash -Algorithm SHA256 plugin.zip # Windows PowerShell
The output must match the published value character for character. If it does not, stop here.
A matching checksum proves file identity, not safety. It only means you have the same file the source has.
2. Look inside without installing
List the contents before unpacking:
unzip -l plugin.zip | less
What to look for:
-
One top-level folder named like the plugin (
my-plugin/). Several unrelated folders, or files at the root, are worth a second look. -
Paths with
../or starting with/. A normal plugin never has them. -
Unexpected file types:
.exe,.sh,.phar,.so, or PHP files with double extensions likeimage.png.php.
Now unpack it into a throwaway folder:
mkdir -p /tmp/zipcheck && unzip -q plugin.zip -d /tmp/zipcheck
cd /tmp/zipcheck
A few quick searches:
# PHP files where there should only be assets
find . -iname "*.php" \( -path "*images*" -o -path "*img*" -o -path "*fonts*" -o -path "*css*" \)
# hidden files and .htaccess
find . -name ".*" -type f
# patterns common in injected code
grep -rnE "eval\s*\(|gzinflate\s*\(|str_rot13\s*\(|shell_exec\s*\(|base64_decode\s*\(" --include=*.php . | head -50
The last search will have false positives — plenty of legitimate plugins use base64_decode for icons or API payloads. What you are looking for is the classic shape of injected code: a long unreadable string fed into eval(base64_decode(...)) or eval(gzinflate(...)), often on the first line of a file or appended after the closing ?>.
3. Read the headers
Every plugin has a main PHP file with a header comment; every theme has one in style.css. Plugins usually repeat the requirements in readme.txt.
grep -rhE "^[ \t/*#@]*(Plugin Name|Theme Name|Version|Requires at least|Tested up to|Requires PHP|WC tested up to):" \
--include=*.php --include=style.css --include=readme.txt . | sort -u
Check three things:
- Version is the one you expected. Mislabeled archives are common — you think you are installing 3.2 and it is actually 2.9.
- Requires PHP is not higher than what your server runs.
- Tested up to is not years behind your WordPress version.
These values are declared by the developer. They are not a test result, but a mismatch is a good reason to ask questions before installing.
4. Run a PHP syntax check
php -l parses a file without running it. It catches truncated downloads, broken re-packs and files that were edited by hand and saved with an error — the kind of thing that gives you a white screen right after activation.
find . -type f -name "*.php" -print0 | xargs -0 -n1 php -l | grep -v "^No syntax errors"
No output means every file parsed. Run it with the same PHP major version as your server, since newer syntax fails on older PHP.
5. Scan with ClamAV
ClamAV is a free, open-source antivirus engine with signatures for common PHP web shells and injected code.
# Debian / Ubuntu
sudo apt install clamav
sudo systemctl stop clamav-freshclam # if the service is running
sudo freshclam # update signatures
sudo systemctl start clamav-freshclam
# macOS
brew install clamav # then follow brew's notes to create freshclam.conf
freshclam
clamscan -r -i /tmp/zipcheck
-i prints only infected files. Look at the summary at the end: Infected files: 0 is what you want.
A clean result means no known signature matched. It is not a code audit, and it will not catch a brand-new or hand-written backdoor — that is why steps 2 and 4 are still worth doing.
6. Read the changelog
Find the entry for this version in readme.txt (== Changelog == section), changelog.txt or CHANGELOG.md. It tells you whether you are about to apply a security fix, a major rewrite, or a release that changes database tables — which decides whether you need a backup and a maintenance window.
7. Install on staging first
Everything above is static. None of it shows how the plugin behaves with your theme, your other plugins and your PHP settings. Install on a staging copy, enable WP_DEBUG_LOG, click through the pages that matter and read wp-content/debug.log before you touch production.
Clean up when you are done:
rm -rf /tmp/zipcheck
Doing it in one command
Steps 1–6 are the same every time, so I put them into a single PHP script: wp-zip-check. It calculates checksums, unpacks safely (skipping ../ and absolute paths), counts files by type, reads the headers, compares the version, runs php -l on every file, scans with ClamAV and prints the changelog entry for that version.
curl -O https://raw.githubusercontent.com/wplend/wp-zip-check/main/wp-zip-check.php
php wp-zip-check.php plugin.zip --expect=1.2.0
It exits with 0 when all checks pass and 1 when one fails, so you can run it over a folder of archives or in CI. --json gives a machine-readable report. It is GPL-licensed, one file, no dependencies besides php-zip and ClamAV.
Quick checklist
- [ ] SHA-256 matches the published value
- [ ] One top-level folder, no
../, no unexpected file types - [ ] No PHP in asset folders, no
eval(base64_decode(...))blobs - [ ] Version, Requires PHP and Tested up to make sense
- [ ]
php -lpasses on every file - [ ] ClamAV: 0 infected files with fresh signatures
- [ ] Changelog read, backup taken
- [ ] Tested on staging
Disclosure: I work on WPlend, where every plugin and theme archive goes through these same automated checks after upload. wp-zip-check is the standalone version of that scanner.
Top comments (0)