Naar inhoud

WordPress steeds opnieuw besmet: de .htaccess-muur

Een klant belde op vrijdag: de site die we dinsdag schoonmaakten serveerde weer pharma-spam. Drie dagen, dezelfde besmetting. Dit is waarom die lus blijft draaien tot je de schrijfbare mappen afsluit.

Jacob Molkenboer
Jacob Molkenboer
Oprichter · A Brand New Company
Gepubliceerd
18 mei 2026
Leestijd
7 min leestijd
Categorie
Security
IJzeren hangslot dicht op een gesloten leren logboek op ivoorkleurig papier, een dun groen lint en gebarsten rood waszegel ernaast

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 de functions.php van een thema.
  • Een rij in wp_options of wp_posts met 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/ | sort

Behandel 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.

Waarschuwing

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 -Indexes

De 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 installs

DISALLOW_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:

  1. Maak een forensische kopie van het hele account voordat je iets aanraakt. Je gaat 'm willen.
  2. Roteer alles: hostingpaneel, SFTP, databasegebruiker, elke WordPress-admin, en de secret keys in wp-config.php.
  3. Vind en verwijder de backdoors. Verifieer de core- en plugin-checksums; alles wat faalt en niet van jou is wordt verwijderd, niet bewerkt.
  4. 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.
  5. Rol de .htaccess-muur en de wp-config.php-vlaggen uit.
  6. Vertrouw je de audit niet volledig, deploy core, plugins en thema's opnieuw vanuit schone bronnen en houd alleen de database en uploads, gescand.
  7. 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.

Veel gestelde vragen

Waarom raakt mijn WordPress-site opnieuw besmet na een professionele opschoning?+
De opschoning haalde de zichtbare payload weg maar liet een backdoor en de oorspronkelijke kwetsbaarheid staan. Zolang één backdoor overleeft en het toegangspunt ongepatcht is, besmetten geautomatiseerde botnets de site binnen minuten opnieuw.
Breekt het blokkeren van PHP in wp-content/uploads iets?+
Nee. WordPress slaat daar alleen media op, nooit uitvoerbare code. Rek het block alleen niet uit naar heel wp-content, want legitieme plugins draaien PHP vanuit hun eigen mappen en dat breekt de site.
Moet ik xmlrpc.php uitzetten?+
Ja, tenzij een dienst die jij beheert het nodig heeft (oudere Jetpack-functies, sommige publicatie-apps). Het is een veelgebruikte versterker voor brute force en pingback-misbruik, en de meeste moderne sites roepen het nooit aan.
Werkt de .htaccess-muur op Nginx?+
Nee. Nginx negeert .htaccess volledig. Je hebt equivalente location-blocks in de serverconfig nodig om PHP-uitvoering in uploads te weigeren en gevoelige bestanden af te sluiten. Het principe is identiek; de syntax niet.

Iets vergelijkbaars bouwen?

Stuur ons één alinea met het proces dat je het meeste tijd kost. Wij reageren met een eerlijk plan — binnen 4u op werkdagen.