049 —Security
Elf .htaccess-directives die blijven, drie die eruit moeten
Een spiekbriefje voor het .htaccess-bestand dat je erfde op die verouderde WordPress- of Magento-site: elf regels die blijven, drie die kwaad doen.
Een Nederlands bureau waar we mee werken erfde vorige maand een WordPress-installatie uit 2014. Het eerste wat de senior developer deed: .htaccess openen in vim, langs het WordPress rewrite-blok scrollen, en 38 regels tellen die een vorige freelancer er door de jaren heen in had geplakt. Twee deden echt nuttig werk. Eén maakte de site actief trager. De rest was opvulling uit oude Stack Overflow-antwoorden.
Als je verouderde sites onderhoudt, is dat bestand waarschijnlijk je belangrijkste beveiligingsartefact. Het draait bij elke request, het overschrijft de serverconfig zonder restart, en het is bijna altijd fout tegen de tijd dat jij het vindt. Hieronder de werkset die wij paraat houden: elf .htaccess-directives die hun plek verdienen op een typische WordPress-, Joomla- of custom-PHP-installatie, plus drie die behulpzaam lijken en je stilletjes pijn doen.
Elf directives die blijven staan
We gaan uit van Apache 2.4 met mod_rewrite, mod_headers en mod_expires aan. Zet ze in de .htaccess van de site-root, tenzij anders vermeld.
1. Forceer HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Plaats dit boven het WordPress-blok. De [L]-flag stopt verdere rewrites op de redirect zelf, wat de loop voorkomt die je krijgt als een latere regel het al doorverwezen request opnieuw rewrite.
2. HSTS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Eén jaar, subdomeinen meegerekend. Sla preload over tot je zeker weet dat elk subdomein HTTPS is en dat ook blijft. Zie MDN over HSTS voor het preload-voorbehoud.
3. Zet directory listing uit
Options -Indexes
Als een map geen indexbestand heeft en Indexes staat aan, dan toont Apache de inhoud. Zo komen klant-PDF's uit een vergeten /uploads/2019/-map alsnog in Google terecht.
4. Bescherm dotfiles
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
Meestal standaard in meegeleverde Apache-configs, maar op shared hosting is het de moeite om expliciet te zijn. Verbreed het patroon naar "^\." als je ook losse .env of .git-bestanden wilt blokkeren.
5. Vergrendel het configbestand
<Files wp-config.php>
Require all denied
</Files>
Joomla's tegenhanger is configuration.php, Drupal gebruikt sites/default/settings.php, Magento 1 heeft app/etc/local.xml. Blokkeer degene die jouw stack ook echt gebruikt.
6. nosniff
Header set X-Content-Type-Options "nosniff"
Voorkomt dat browsers de Content-Type die jij stuurt gaan tegenspreken, het mechanisme achter een klasse MIME-confusion-aanvallen op door gebruikers geüploade bestanden.
7. Frame deny
Header set X-Frame-Options "SAMEORIGIN"
Of zet Content-Security-Policy: frame-ancestors 'self' als je al een CSP hebt. Kies er één, niet allebei, anders kunnen browsers het oneens worden over de volgorde.
8. Referrer-policy
Header set Referrer-Policy "strict-origin-when-cross-origin"
Snijdt de lange staart af van sessietokens en admin-URL's die anders in de Referer-header lekken wanneer een redacteur op een externe link klikt.
9. Blokkeer xmlrpc.php
<Files xmlrpc.php>
Require all denied
</Files>
Tenzij je nog de Jetpack mobile-API of pingback gebruikt, heb je hem niet nodig. xmlrpc.php is het meest brute-forced endpoint op het moderne WordPress-web.
10. Geen PHP-uitvoering in uploads
# wp-content/uploads/.htaccess (of sites/default/files/.htaccess op Drupal)
<FilesMatch "\.(php|phtml|phps|php\d+)$">
Require all denied
</FilesMatch>
Dit is de directive die voorkomt dat een kwetsbaar uploadformulier in remote code execution verandert. Hij hoort in de uploads-map, niet in de site-root.
11. Beperk de request body
LimitRequestBody 10485760
10 MB. Kies een getal dat past bij je werkelijke uploadgrootte en bij je PHP upload_max_filesize. De default is ongelimiteerd, en zo loopt één curl-loop op een vergeten endpoint de disk leeg.
Drie directives die je stilletjes pijn doen
Deze gooien we er meteen uit. Elk ziet er beveiligend uit. Elk maakt de situatie actief slechter.
Wildcard CORS
Header set Access-Control-Allow-Origin "*"
Bijna altijd erin geplakt om een font-loading-fout tijdens development op te lossen, en daarna vergeten. Op een site met cookie-gebaseerde admin-endpoints is een wildcard origin in combinatie met Access-Control-Allow-Credentials een klasse data-exfiltratiebug. De CORS-doc van MDN legt de precieze interactie uit. Heb je écht cross-origin font-toegang nodig, scope het dan: Header set Access-Control-Allow-Origin "https://cdn.example.com".
Jaarlange cache op HTML
ExpiresActive On
ExpiresDefault "access plus 1 year"
Gekopieerd uit een 'maak je site sneller'-tutorial. Prima voor fingerprinted assets zoals app.a1b2c3.css. Slecht voor HTML, want die blijft dan een jaar lang in browser- en CDN-caches hangen ongeacht wat jij publiceert. Scope Expires altijd op MIME-type en houd HTML op een korte policy:
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
Apache 2.2-access-syntax op een 2.4-server
Order allow,deny
Deny from all
Allow from 81.23.45.67
Op Apache 2.4 werkt dit alleen als mod_access_compat geladen is. Bij veel moderne hosts is dat niet zo, en dan doet het blok stilletjes niets terwijl het lijkt op een IP-allowlist. De huidige syntax is Require ip 81.23.45.67 binnen het relevante <Files>- of <Directory>-blok. De 2.4-upgrade-notes van Apache bevatten de volledige mapping.
Testen voor je opslaat
Een typo in .htaccess gooit de site bij de volgende request offline. Er is geen compileerstap. Twee gewoonten die ons hebben gered:
Ten eerste: bewerk het live bestand nooit blind. Trek hem eerst lokaal binnen, bewerk lokaal, en draai apachectl -t tegen een matching lokale Apache. Lukt dat niet, dan httpd -t op de server zelf voordat de wijziging live gaat.
Ten tweede: maak voor elke bewerking een gedateerde back-up: cp .htaccess .htaccess.2026-05-27. De meeste productiestoringen waar we klanten uit hebben geholpen waren één regel revert verwijderd, mits er een back-up bestond.
Toen we Pier bouwden liepen we hier op klantservers tegenaan: het bureau wist pas dat een .htaccess-bewerking iets had gebroken als een klant belde. Wat we uiteindelijk hebben gedaan is van elk bestand dat Pier aanraakt een snapshot bewaren in de version history, zodat een one-click revert altijd binnen handbereik is, ook als de FTP-client al dicht is.
Het kleinste wat je vandaag kunt doen: open de .htaccess van je oudste actieve site, sla een gedateerde kopie ernaast op, en verwijder de wildcard-CORS-regel als die erin staat. De andere tien directives mogen tot morgen wachten.
— Vragen —
Heb ik zowel X-Frame-Options als CSP frame-ancestors nodig?
Kies er één. CSP frame-ancestors heeft in moderne browsers voorrang op X-Frame-Options, maar X-Frame-Options op zichzelf is prima als je nog geen CSP hebt.
Breekt het blokkeren van xmlrpc.php Jetpack?
Het breekt de Jetpack mobile-app en pingback. Als geen van beide op de site in gebruik is, is de blokkering veilig en verdwijnt meteen het meest brute-forced endpoint.
Kan ik .htaccess helemaal uitzetten en de hoofdconfig gebruiken?
Ja, als je de server zelf beheert. Op shared hosting kan dat meestal niet. Daar is .htaccess vaak het enige beschikbare override-mechanisme.