
🌐 Also available in: Deutsch · Français · Español
A WordPressuploads folder is supposed to hold your holiday photos
and the occasional PDF. It is emphatically not supposed to hold a PHP
web shell — but “supposed to” is doing a lot of heavy lifting there. The
uploads directory is the single most common place a compromised WordPress ends
up executing attacker code, because it’s writable, web-reachable, and everyone
forgets about it. So I gave mine a nose: a virus scanner that sniffs the upload
folder every night and emails me the moment it smells something. 👃 And because a smoke detector you’ve never tested is just ceiling jewellery, I
also made it go off on purpose. More on that at the end.🎯 The Design (deliberately boring)
- 🌙 On-demand, nightly. Not a real-time on-access daemon — that’s heavy for a modest VPS and I’m basically the only person who ever uploads anything. A once-a-night sweep is the right trade-off: worst case, malware sits for a few hours before the morning email, not weeks.
- 🔇 Silent unless it finds something. Same philosophy as everything else on this box (rkhunter, Monit): a clean scan sends nothing. A mail from the scanner therefore always means “look at this now,” never “everything’s fine, as usual.”
- 🔎 Detection only. It reports, it does not delete or quarantine. On a personal blog I’d rather eyeball a finding than have an automated janitor silently eat a false-positive file.
- 🧩 Ansible-managed (role
blog_clamav), so it’s reproducible and version-controlled, not a snowflake script I’ll forget how I built.
📦 How It’s Installed & Configured
On AlmaLinux 9, the whole thing is ClamAV plus its signature updater:dnf install clamav clamav-updateThe packaged freshclam.conf ships with a placeholder line that
stops it from ever running — the classic first-time gotcha. Remove it,
pull the signature database once, and enable auto-updates:# kill the "Example" line or freshclam refuses to start
sed -i '/^Example/d' /etc/freshclam.conf
freshclam # first full signature download
systemctl enable --now clamav-freshclam # keep signatures fresh automaticallyThe actual scan is a small script at /usr/local/sbin/wordpress-clamav-scan.sh. It scans only the uploads
directory, plays nice with the rest of the box, skips absurdly large files
(malware isn’t hiding in a 300 MB video), and — the important part
— only emails on a hit:#!/bin/bash
SCAN_LOG="/var/log/clamav/wordpress-scan.log"
UPLOADS_DIR="/srv/wordpress-www/wp-content/uploads"
nice -n 19 ionice -c3 clamscan --recursive --infected --no-summary
\
--max-filesize=200M --max-scansize=200M
\
"$UPLOADS_DIR" > "$SCAN_LOG" 2>&1
RC=$?
# clamscan exit code 1 = something was found -> mail it, otherwise stay silent
if [ "$RC" -eq 1 ]; then
mail -s "[$(hostname -f)] ClamAV: infected file(s) found in WordPress uploads"
\
me@example.com < "$SCAN_LOG"
fi
exit 0A cron entry runs it every night at 02:45. Mail goes out through the same msmtp relay the rest of my alerts use (rkhunter, Monit), so there’s
one mail path to keep working, not three.45 2 * * * /usr/local/sbin/wordpress-clamav-scan.sh🧪 Making It Bark: the EICAR Test
Here’s the fun bit. You do not download real malware to test an antivirus — you use EICAR, an industry-standard 68-byte test string that every scanner on earth is contractually obliged to flag, and which is completely harmless (it’s not executable malware, it’s a printable ASCII string designed purely to trip detection). Drop it in the scan path, run the real script, watch the whole chain fire, then delete it:# drop the harmless EICAR test file into uploads
printf 'X5O!P%%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*\n'
\
> /srv/wordpress-www/wp-content/uploads/eicar-test.txt
# run the actual nightly script by hand
/usr/local/sbin/wordpress-clamav-scan.sh
# clean up immediately
rm -f /srv/wordpress-www/wp-content/uploads/eicar-test.txtClamAV spotted it instantly — Eicar-Signature FOUND —
the script hit exit code 1, fired the mail, and moments later this landed in my
inbox:From: homelab@apt-upgrade.me
Subject: [www.apt-upgrade.me] ClamAV: infected file(s) found in WordPress uploads
/srv/wordpress-www/wp-content/uploads/eicar-test.txt: Eicar-Signature FOUNDAnd to be thorough I checked the mail relay’s own log, which confirmed the
handoff rather than just assuming it:host=smtp.strato.de tls=on auth=on ... recipients=you@example.com
smtpstatus=250 ... 'OK queued' exitcode=EX_OKDetection ✔️, the real script’s mail path ✔️, delivery to my actual inbox ✔️.
That’s the difference between “I configured a scanner” and “I have a scanner
that works” — the second one you’ve watched catch something. 🎯 (Test file
deleted right after, uploads clean again. This was a drill, not a breach.)⚠️ Honest Limitations
- It’s a nightly scan, not real-time. A malicious upload is caught at the next 02:45 run, not the instant it lands.
- It scans uploads, the highest-risk folder — not the whole disk. Different tool for the OS layer (that’s rkhunter’s job here).
- Detection, not removal. The email is the whole product; cleanup is a human decision.





