WordPress .htaccess absichern – mein Security-Template

Fast jede WordPress-Installation, die ich übernehme, hat eine .htaccess mit genau dem Block, den WordPress selbst zwischen # BEGIN WordPress und # END WordPress schreibt. Mehr nicht. Das reicht für Permalinks, aber nicht um die Seite gegen die üblichen Angriffsmuster zu härten – Author-Enumeration, Uploads-Execution, XML-RPC-Missbrauch, Scanner mit leerem User-Agent.
Ich nutze seit Jahren eine Basis-.htaccess, die ich auf jeder neuen Installation ablege. Hier ist die aktuelle Version mit Erklärung zu jedem Block.

Directory Listing und Dotfiles

Das ist der erste Block in jeder meiner Konfigurationen. Options -Indexes verhindert, dass Apache bei fehlender Index-Datei einfach den Ordnerinhalt ausliefert. Klingt trivial, ist aber der häufigste Grund dafür, dass Angreifer Plugin-Ordner und Upload-Verzeichnisse durchsuchen können.
Dotfiles wie .git, .env oder .DS_Store haben auf einem Webserver nichts zu suchen – trotzdem finde ich sie regelmäßig öffentlich erreichbar.

Options -Indexes

<FilesMatch "^\.">
	Require all denied
</FilesMatch>

wp-config.php und Backup-Varianten

Die Standard-Regel auf deckt nur die eine Datei ab. Problem: Backup-Plugins, FTP-Clients und Editoren legen Varianten an – wp-config.php.bak, wp-config.php.swp, wp-config.old. Die sind weiterhin lesbar, weil sie nicht mehr auf .php enden und Apache sie als Klartext ausliefert.
Deshalb nutze ich ein FilesMatch-Pattern, das alle gängigen Backup-Endungen abdeckt.

<FilesMatch "^wp-config\.(php|php~|save|swp|swo|bak|old|orig|original|tmp)$">
	<IfModule mod_authz_core.c>
		Require all denied
	</IfModule>
	<IfModule !mod_authz_core.c>
		Order deny,allow
		Deny from all
	</IfModule>
</FilesMatch>

Die doppelte IfModule-Struktur ist Absicht – Apache 2.4 nutzt mod_authz_core, ältere Installationen noch Order deny,allow. So funktioniert die Regel auf beiden.

Script-Execution in Uploads blockieren

Der klassische Angriffsvektor: Ein Plugin hat eine Upload-Lücke, der Angreifer lädt eine PHP-Datei in wp-content/uploads/ hoch und ruft sie direkt auf. Damit das scheitert, blockiere ich alle Script-Endungen in diesem Pfad – nicht nur .php, sondern auch .phtml, .phar und die selteneren Varianten .php3 bis .php8.

<IfModule mod_rewrite.c>
	RewriteCond %{REQUEST_URI} ^/wp-content/uploads/.*\.(php[0-9]?|phtml|phar)$ [NC]
	RewriteRule .* - [F,L]
</IfModule>

wp-includes absichern

In wp-includes/ stehen Core-Dateien, die nie direkt aufgerufen werden sollten. Ausnahmen sind ein paar TinyMCE-Sprachdateien und theme-compat – die brauche ich aber auch blockieren, weil sie historisch für Angriffe genutzt wurden.

<IfModule mod_rewrite.c>
	RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
	RewriteRule ^wp-includes/js/tinymce/langs/.+\.php$ - [F,L]
	RewriteRule ^wp-includes/theme-compat/ - [F,L]
</IfModule>

Author-Enumeration

WordPress hat eine nervige Eigenart: Ruft man ?author=1 auf, redirected WordPress auf die Autorenseite und der Username taucht im URL-Pfad auf. Für Angreifer ist das ein freier Username-Dump – sie brauchen dann nur noch das Passwort.
Ein Rewrite-Block löst das:

<IfModule mod_rewrite.c>
	RewriteCond %{QUERY_STRING} (author=\d+) [NC]
	RewriteRule .* - [F,L]
</IfModule>

Wenn du öffentliche Autorenarchive brauchst, verlinke sie über den Slug (/author/mein-name/) statt über die ID – das bleibt erreichbar.

XML-RPC und Readme-Dateien

XML-RPC wird von fast keiner modernen WordPress-Installation mehr aktiv genutzt, ist aber ein beliebtes Ziel für Brute-Force-Angriffe, weil sich dort hunderte Login-Versuche in einem einzigen Request bündeln lassen. Ich schalte es komplett aus – wenn eine App das wirklich braucht, kann man es gezielt wieder freigeben.
readme.html und license.txt verraten die exakte WordPress-Version. Weg damit.

<Files xmlrpc.php>
	<IfModule mod_authz_core.c>
		Require all denied
	</IfModule>
</Files>

<FilesMatch "^(readme|license)\.(txt|html)$">
	<IfModule mod_authz_core.c>
		Require all denied
	</IfModule>
</FilesMatch>

Scanner und Bad Bots blockieren

Scanner wie sqlmap, nikto oder masscan identifizieren sich meistens selbst über ihren User-Agent – weil die Entwickler davon ausgehen, dass sie auf Servern laufen, wo das egal ist. Ich filtere diese User-Agents raus, genauso wie leere User-Agents, die bei seriösen Clients praktisch nie vorkommen.

<IfModule mod_rewrite.c>
	RewriteCond %{HTTP_USER_AGENT} ^$ [OR]
	RewriteCond %{HTTP_USER_AGENT} (libwww-perl|wget|python-requests|scan|masscan|nikto|sqlmap|nmap|fuzz|havij|acunetix) [NC]
	RewriteRule .* - [F,L]
</IfModule>

curl habe ich bewusst draußen gelassen – Monitoring-Tools und Deploy-Scripts nutzen curl häufig für Healthchecks.

Security-Header

Der letzte Block setzt die Standard-Security-Header. Wichtig: Strict-Transport-Security nur aktivieren, wenn die Seite wirklich komplett auf HTTPS läuft – inklusive aller Subdomains. Sonst sperrst du dich selbst aus, wenn mal was auf HTTP erreichbar sein soll.
X-XSS-Protection: 0 sieht falsch aus, ist aber korrekt. Moderne Browser ignorieren den Header ohnehin, und in der Vergangenheit hat er mehr Probleme verursacht als gelöst. Der Wert 0 deaktiviert ihn explizit.

<IfModule mod_headers.c>
	Header always set X-Frame-Options "SAMEORIGIN"
	Header always set X-Content-Type-Options "nosniff"
	Header always set Referrer-Policy "strict-origin-when-cross-origin"
	Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
	Header always set Cross-Origin-Opener-Policy "same-origin"
	Header always set X-XSS-Protection "0"
	Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
	Header always unset X-Powered-By
	Header always unset Server
</IfModule>

Reihenfolge im File

Meine Security-Regeln stehen immer vor dem WordPress-Block. Apache arbeitet die Datei von oben nach unten ab – ein Request, der durch eine Security-Regel geblockt wird, kommt nie bei den WordPress-Rewrite-Rules an. Umgekehrt wäre es wirkungslos.

Was ich bewusst weglasse

Ein paar Dinge, die in vielen Tutorials auftauchen und bei mir trotzdem nicht drinstehen:

  • IP-Whitelist für wp-admin – nur sinnvoll bei festen IPs. Bei Kunden mit mobilen Mitarbeitern sperrt man damit ständig Leute aus.
  • Limit auf HTTP-Methoden – WordPress braucht mehr als nur GET und POST, vor allem mit REST-API. Schnell kaputt zu machen.
  • Bot-Blocking per BrowserMatch – übernimmt bei mir ein eigenes Plugin, das Logs auswertet und dynamisch nachzieht.

Fazit

Die .htaccess ist der günstigste Security-Layer, den WordPress hat – keine PHP-Ausführung, keine Datenbank-Queries, einfach Apache-Regeln. Der gezeigte Template-Stand deckt die häufigsten Angriffsmuster ab, die ich in Logs meiner Kunden sehe. Security-Plugins ersetzt das nicht, aber es verhindert, dass triviale Scans überhaupt bis zu WordPress durchkommen.