Een klant belde op een vrijdagmiddag. De WordPress-site die we dinsdag hadden schoongemaakt serveerde weer pharma-spam. Dezelfde geïnjecteerde <head>, hetzelfde hostingaccount, dezelfde rommel-URL's in de index van Google. Drie dagen schoon, daarna opnieuw besmet en terug bij af.
Dit is de meest voorkomende securitytelefoon die we krijgen op verouderde WordPress. Niet "we zijn gehackt", maar "we zijn alwéér gehackt, nadat we iemand betaald hebben om het op te lossen". Die herbesmetting is geen pech. Het betekent dat de vorige opschoning het symptoom weghaalde en het mechanisme liet draaien.
Waarom een schoongemaakte site opnieuw besmet raakt
Een malwarescan haalt weg wat je ziet: de spamlinks, de geïnjecteerde JavaScript, de herschreven index.php. Zelden haalt-ie de backdoor weg. Een backdoor is een klein PHP-bestand, of een paar regels vastgeschroefd aan een legitiem bestand, waarmee een aanvaller naar je site kan schrijven wanneer hij wil. Zolang één backdoor overleeft en het gat dat ze binnenliet nog openstaat, gaat de herbesmetting vanzelf. Een botnet heeft je domein al op een lijst staan. Het komt terug in minuten, niet in weken.
De payload die je opruimde was nooit het punt. Het punt is persistentie, en persistentie verbergt zich meestal op een van deze plekken:
- Een PHP-bestand gedropt in
wp-content/uploads, een map waar WordPress alleen afbeeldingen en PDF's verwacht. - Een nep-must-use-plugin in
wp-content/mu-plugins, die bij elke request laadt en nooit op het Plugins-scherm verschijnt. - Een paar toegevoegde regels in
wp-config.php,wp-load.php, of defunctions.phpvan een thema. - Een rij in
wp_optionsofwp_postsmet een base64-blob die een backdoor op aanvraag opnieuw uitleest.
En de saaie: een tweede, vergeten site in hetzelfde hostingaccount. Gedeelde document roots besmetten elkaar opnieuw. Eén site schoonmaken en de verlaten staging-kopie ernaast negeren is de helft van de vloer dweilen.
Vind het mechanisme voordat je de muur bouwt
De muur werkt niet als je deze stap overslaat. Sluit je de deuren met een bekende indringer nog binnen, dan heb je hem net opgesloten bij je data.
Begin met de installatie vergelijken met een schone WordPress. WP-CLI doet dit in twee commando's en is de snelste manier om te zien wat er niet hoort:
# From the WordPress root, with WP-CLI installed
wp core verify-checksums
wp plugin verify-checksums --all
# PHP files changed since you went live this month
find . -type f -name "*.php" -newermt "2026-05-01" -print | sort
# Common backdoor signatures (leads, not proof)
grep -RIlE "eval\(|base64_decode\(|gzinflate\(|str_rot13\(" \
--include="*.php" wp-content/ | sortBehandel de grep-output als aanwijzingen, niet als oordeel. Moderne backdoors splitsen eval over string-concatenatie juist om dat patroon te verslaan. De checksum-diff en de lijst met "recent gewijzigd" zijn betrouwbaarder, want die beschrijven de staat, niet de intentie.
De .htaccess-regels hieronder werken alleen op Apache (of LiteSpeed, dat dezelfde bestanden leest). Op Nginx doen ze niets en heb je de equivalente location-blocks nodig. En geen enkele .htaccess-regel maakt een besmette site schoon. Het stopt de volgende besmetting van uitvoeren. Je moet de backdoor eerst nog vinden en verwijderen.
De .htaccess-muur
De denkomslag die de lus beëindigt: ga ervan uit dat er opnieuw een kwaadaardig PHP-bestand in een schrijfbare map belandt, en haal het vermogen weg om uit te voeren wanneer dat gebeurt. Een uploadmap vol .php-bestanden is onschadelijk als de server weigert daar PHP uit te voeren.
Laag één gaat in de uploadmap zelf. Maak wp-content/uploads/.htaccess aan:
# wp-content/uploads/.htaccess (Apache 2.4+)
<FilesMatch "\.(?i:php|phtml|php3|php4|php5|php7|phps|pht|inc)$">
Require all denied
</FilesMatch>Schakel PHP niet pauschal uit over heel wp-content. Plugins voeren legitiem code uit vanuit hun eigen mappen, en je breekt de site. De uploads-tak is de veilige plek om absoluut te zijn, want daar hoort niets ooit te draaien.
Laag twee gaat in de root-.htaccess, boven het # BEGIN WordPress-block zodat WordPress het niet overschrijft:
# Lock files attackers read or write
<FilesMatch "^(wp-config\.php|\.htaccess|\.user\.ini|readme\.html|license\.txt)$">
Require all denied
</FilesMatch>
# Disable XML-RPC unless a service you control needs it
<Files xmlrpc.php>
Require all denied
</Files>
# No directory listings
Options -IndexesDe FilesMatch-directive staat beschreven in de Apache core module reference; het matcht op bestandsnaam ongeacht het request-pad, precies wat je wilt voor bestanden die nooit direct opgevraagd horen te worden.
Het XML-RPC-block staat weer volop in de belangstelling in de WordPress-community, en met reden. xmlrpc.php is de klassieke versterker voor credential stuffing en pingback-misbruik: één request, honderden wachtwoordpogingen. Tenzij je de Jetpack-app draait of een publicatieclient die er echt van afhangt, haalt uitzetten een hele categorie ruis weg uit je logs en je CPU-grafiek.
Sluit ook de editor
De muur zit op filesystem-niveau. Voeg er één regel op applicatieniveau aan toe nu je toch bezig bent, want een gecompromitteerd adminaccount dat de ingebouwde thema-editor gebruikt is een backdoor met een UI. In wp-config.php:
// wp-config.php, above the "stop editing" line
define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true ); // also blocks plugin/theme installsDISALLOW_FILE_MODS is agressief: het blokkeert updates en installs via het dashboard, dus gebruik het alleen op sites waar je ergens anders vandaan naartoe deployt. De eerste regel hoort echter op vrijwel elke productie-installatie. De officiële WordPress-hardeninggids raadt het al jaren aan, en het kost niets.
De volgorde die de lus echt stopt
Volgorde telt zwaarder dan welke losse regel ook. Wij draaien herbesmettingszaken in deze volgorde, elke keer:
- Maak een forensische kopie van het hele account voordat je iets aanraakt. Je gaat 'm willen.
- Roteer alles: hostingpaneel, SFTP, databasegebruiker, elke WordPress-admin, en de secret keys in
wp-config.php. - Vind en verwijder de backdoors. Verifieer de core- en plugin-checksums; alles wat faalt en niet van jou is wordt verwijderd, niet bewerkt.
- Dicht het toegangspunt. Het is bijna altijd een verouderde plugin of een al lang verlaten premium-thema. Update het, of ruk het eruit als de leverancier weg is.
- Rol de .htaccess-muur en de
wp-config.php-vlaggen uit. - Vertrouw je de audit niet volledig, deploy core, plugins en thema's opnieuw vanuit schone bronnen en houd alleen de database en uploads, gescand.
- Houd de logs 7 tot 14 dagen in de gaten. Herbesmetting die erdoorheen komt laat zich snel zien; stilte na twee weken betekent meestal dat je 'm te pakken had.
Stap vier en vijf zijn de stappen die de goedkope opschoningen overslaan. De muur zonder de patch koopt je een paar rustige dagen. De patch zonder de muur houdt tot de volgende zero-day in de volgende vergeten plugin.
Toen we vorig jaar een verouderde WordPress-redding overnamen voor een Nederlandse logistieke klant, bleek de herbesmetting een mu-plugin die drie eerdere opschoningen nooit hadden geopend, ladend bij elke request en twee bestanden per dag herschrijvend. De .htaccess-muur hield de linie terwijl wij hem vonden en het toegangspunt fatsoenlijk herbouwden.
De vijfminutenversie die je vandaag kunt draaien: SSH het account in, draai wp core verify-checksums, en lijst dan elk PHP-bestand onder wp-content/uploads. Geeft een van beide commando's iets terug, dan heb je je antwoord al over waarom het steeds terugkomt.




