Mehr Sicherheit für WordPress per htaccess

Die Zeiten ändern sich.

Dieser Beitrag scheint älter als 8 Jahre zu sein – eine lange Zeit im Internet. Der Inhalt ist vielleicht veraltet ...

Code-Snippets sind etwas Tolles. Du suchst nach einem Problem, findest ein paar Zeilen Code, fügst sie an der richtigen Stelle ein und – „Magic!“ – es funktioniert. Doof nur, wenn jetzt irgendwo anders etwas kaputt geht. Warum geht das denn jetzt nicht mehr? Ich habe doch nur diesen Code eingefügt, der was ganz anderes macht …

Code-Snippets solltest du nur verwenden, wenn du sie komplett verstehst. Es folgt daher eine Sammlung für die .htaccess, die deine WordPress-Installation sicherer machen, inklusive Erklärung.

TL;DR: WordPress kann per .htaccess abgesichert werden. Nur benutzen, wenn du weißt, was du tust. Wenn nicht, den Artikel weiterlesen. Das komplette Gist findest du bei Github.

Gehen wir den Code einzeln durch:

# Don't show errors which contain full path diclosure (FPD)
# Use that line only if PHP is installed as a module and not per CGI
# try using a php.ini in that case.
# Change mod_php5.c to mod_php7.c if you are running PHP7
<IfModule mod_php5.c>
  php_flag display_errors Off
</IfModule>

Die If-Abfragen greifen nur, wenn PHP als Modul und nicht als CGI genutzt wird und müssen bei Nutzung von PHP 7 gegebenenfalls umgestellt werden (mod_php5.c durch mod_php7.c ersetzen). Ist PHP also als Modul vorhanden, dann greift die If-Abfrage und es wird per PHP-Flag eine Einstellung gesetzt: „display_errors Off“. Diese Einstellung bewirkt, dass PHP-Fehlermeldungen nicht mehr ausgegeben werden und so wichtige Informationen verraten, wie zum Beispiel den Installationspfad.

# Protect XMLRPC (needed for Apps, Offline-Blogging-Tools, Pingback, etc.)
# If you use that, these tools will not work anymore
<Files xmlrpc.php>
  Order Deny,Allow
  Deny from all
</Files>

Die XMLRPC-Datei ist eine Schnittstelle für Blogging-Tools, sowie für die Pingbacks. Sie ist seit WordPress 3.5 standardmäßig aktiviert und es gibt kein UI mehr, um es zu deaktivieren. Mit diesen Zeilen wird der Zugriff auf diese Datei unterbunden. Folglich gibt es keine Pingbacks mehr und Blogging-Tools wie zum Beispiel die iOS- und Android-App funktionieren nicht mehr. Warum diese Schnittstelle trotzdem deaktiviert werden sollte, liegt an dem möglichen Sicherheitsproblem. 500 Passwörter können über diese Schnittstelle in einem Schwung übermittelt werden. Und der Filter zum Deaktivieren scheint aktuell nicht ausreichend zu sein, da eine Abfrage den Server trotzdem belasten kann.

# Don't list directories
<IfModule mod_autoindex.c>
  Option -Indexes
</IfModule>

Sofern das Modul vorhanden ist, wird mit dieser Einstellung die standardmäßige Anzeige des Verzeichnisinhalts deaktiviert. Eigentlich sollte in WordPress in jedem Ordner dafür eine leere index.php liegen, aber es könnte andere Ordner geben, deren Inhalt wir nicht einfach preisgeben möchten.

# Protect all readme.txt files from all plugins
<Files readme.txt>
  Order allow,deny
  Deny from all
</Files>

Hiermit wird der Zugriff auf alle readme.txt-Dateien unterbunden. Alle Plugins aus dem Repo haben diese Datei und geben somit ihre Versionsnummer heraus. Tools wie WPScan lesen diese Datei aus und erkennen so die Plugins und geben Hinweise auf mögliche Schwachstellen. Das hat nur eine geringe Sicherheitsfunktion, denn die meisten Angriffe machen sich nicht die Mühe zu testen ob ein bestimmtes Plugin (oder überhaupt WordPress) genutzt wird. Sie feuern einfach wild los und treffen bei vielen Versuchen zumindest ein paar mal auf eine Website, wo das fragliche Plugin in genau der Version mit der Sicherheitslücke benutzt wird. Aber da der Schutz dieser Datei sehr einfach ist, kann das durchaus gemacht werden. Eine Information weniger, die preisgegeben wird.

# Protect wp-config.php and other files
<FilesMatch "(.htaccess|.htpasswd|wp-config.php|liesmich.html|readme.html)">
  Order deny,allow
  Deny from all
</FilesMatch>

Neben den readme.txt-Dateien der Plugin gibt es noch ein paar weitere Dateien, die schützenswert sind. .htaccess/.htpasswd werden gerne für serverseitigen Passwort genutzt, wer zum Beispiel seine wp-login.php so geschützt hat, sollte die entsprechenden Konfigurationsdateien ebenfalls absichern. In der wp-config.php stehen die Zugangsdaten zur Datenbank, auch sie sollte extra gesichert werden. Und die liesmich.html und readme.html geben die WordPress-Version preis. Update: Das war früher so. Inzwischen ist die Versionsnummer aus dieser Datei entfernt worden. Wiederum keine Information, die nicht auch anders heraus zu bekommen ist, aber warum nicht.

# Block the include-only files.
# Do not use in Multisite without reading the note in Codex!
# See: http://codex.wordpress.org/Hardening_WordPress#Securing_wp-admin
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^wp-admin/includes/ - [F,L]
  RewriteRule !^wp-includes/ - [S=3]
  RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
  RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
  RewriteRule ^wp-includes/theme-compat/ - [F,L]
</IfModule>

Direkt aus dem Codex-Artikel über das Absichern von WordPress. So wird der Zugang zu diversen Dateien des WordPress-Core verhindert, die niemals von außen benutzt werden sollten.

# Set some security related headers
# See: http://de.slideshare.net/walterebert/die-htaccessrichtignutzenwchh2014 (GERMAN)
<IfModule mod_headers.c>
  Header set X-Frame-Options SAMEORIGIN 
  Header set X-Content-Type-Options nosniff 
  Header set X-XSS-Protection "1; mode=block" 
  Header set Content-Security-Policy "default-src 'self'; img-src 'self' http: https: *.gravatar.com;"
</IfModule>

Jetzt wird es spannend! Hier setzen wir (wenn möglich) vier neue Header. Ich gehe sie einzeln durch:

X-Frame-Options SAMEORIGIN
Mit dieser Einstellung wird definiert, ob die Website in einem Frame oder iFrame dargestellt werden darf. Gültige Werte sind DENY zum kompletten Deaktivieren der Funktion. SAMEORIGIN erlaubt das Einbauen in einen Frame nur der eigenen Domain. Und mit ALLOW-FROM https://example.com/ können einzelne Websites freigegeben werden. Problematisch ist hier vor allem, dass die neue Embedding-Funktion nicht mehr funktioniert, wenn Frames nicht erlaubt sind.

Update: Nach einer Rückfrage von Rouven Hurling und Dank einem Gist von Sergej Müller gibt es nun auch eine Lösung, mit der die Embed-Funktion erhalten bleibt:

<IfModule mod_setenvif.c>
    SetEnvIf Request_URI "/embed/$" IS_embed
    <IfModule mod_headers.c>
    	Header set X-Frame-Options SAMEORIGIN env=!REDIRECT_IS_embed
    </IfModule>
</IfModule>

X-Content-Type-Options nosniff
Mit dieser Einstellung wird das Mime-Type-Sniffing deaktiviert und so ein möglicher Angriffsvektor unterbunden. So werden zum Beispiel Drive-by-Downloads verhindert.

X-XSS-Protection "1; mode=block"
Auch hier gibt es keine großen Einstellmöglichkeiten. 0 deaktiviert den Cross-Site-Scripting-Schutz und 1 aktiviert ihn. Mit mode=block wird definiert, dass im Fall eines XSS die Anfrage blockiert werden soll.

Content-Security-Policy
Wohl die spannendste und gefährlichste Einstellung in diesem Block. Mit dieser Policy kann für verschiedene Dateitypen (Bilder, Skripte, Schriften, etc.) definiert werden, aus welchen Quellen sie geladen werden dürfen. So kann zum Beispiel das Laden von Skripten auf den eigenen Server beschränkt werden. Klingt erst einmal großartig. XSS-Angriffe laufen so ins Leere. Aber der Teufel steckt im Detail, denn manche Dateien werden ja nun mal von anderen Servern geladen. Gravatar-Bilder werden vom Gravatar-Server ausgeliefert, Google-Fonts von Google.com geladen und externe Analytics-Dienste liegen ebenfalls beim entsprechenden Anbieter …

Eine gute Einführung gibt es beim Betreiber von Securityheaders.io Scott Helme. Insbesondere weist er auf die Testmöglichkeit per Content-Security-Policy-Report-Only: hin. Damit kann die gesamte Konfiguration erst einmal getestet werden (die Fehler-Konsole des Browsers verrät gebenenfalls über blockierte Elemente), bevor am Ende mit der richtigen Policy die Einstellung konkret gesetzt wird.

Aber Achtung! Ein Fehler in der Syntax kann schnell zu einem Internal Server Error (500) führen. Nicht erschrecken lassen, sondern schnell korrigieren oder die Zeile mit „#“ auskommentieren. Danke an dieser Stelle an Andreas Hecht von drweb.de für seinen Linktipp zu securityheaders.io.

Eine komplette Dokumentation zu allen Einstellungsmöglichkeiten inklusive Browser-Support findest du auf http://content-security-policy.com/. Wer deutschsprachige Quellen sucht, der findet eine Artikelreihe dazu bei Heise.de

#Force secure cookies (uncomment for HTTPS)
<IfModule mod_headers.c>
  #Header edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure
</IfModule>

Bei SSL-verschlüsselten Websites empfiehlt es sich auch die Cookies per https zu schützen. In diesem Fall die Raute entfernen.

#Unset headers revealing versions strings
<IfModule mod_headers.c>
  Header unset X-Powered-By
  Header unset X-Pingback
  Header unset SERVER
</IfModule>

Mit diesen Zeilen werden (sofern möglich) die angegebenen Header entfernt, da sie die Versionsnummern von verwendeten Komponenten preisgeben, wie zum Beispiel der Server-Software oder der PHP-Version. Ob die Einstellung wirklich greift sollte allerdings nach dem Einbau noch einmal kontrolliert werden. Alternativ kann der Wert auch überschrieben werden, wenn ein Löschen nicht möglich ist.

# Disable PHP execution in /uploads
<IfModule mod_rewrite.c> 
  RewriteEngine On 
  RewriteBase / 
  RewriteRule ^(wp-content/uploads/.+.php)$ $1 [H=text/plain] 
</IfModule> 

Ist das Kind in den Brunnen gefallen und wurde eine Sicherheitslücke ausgenutzt, dann hat der Angreifer womöglich eine Backdoor installieren können. Zum Beispiel in den einzigen Bereich wo wir Schreibrechte vergeben müssen, im Uploads-Ordner. Nur haben dort PHP-Dateien sicher nichts verloren (Ausnahmen bestätigen die Regel), daher sorgt die obige Anweisung dafür, dass PHP-Dateien im Uploads-Ordner nicht ausgeführt, sondern als reiner Text zurückgeliefert werden. Alternativ könnte man auch mit einem 403 Forbidden antworten.

Wenn du dir sicher bist, dass kein Plugin direkt aufgerufen werden muss, dann kann dieser Schutz auch auf den gesamten /wp-content-Ordner ausgeweitet werden (obwohl dann lieber nicht mit der Plain-Text-Ausgabe). Ein Aufrufen per require oder include ist davon nämlich nicht betroffen.

Update: Ich war selber nicht zufrieden mit dieser Lösung und nach dem Kommentar von Klaus Kruse habe ich das wieder aus dem Gist entfernt. Ich empfehle lieber die oben beschriebene Lösung den /wp-content-Ordner bzw. den /uploads-Ordner mit einer .htaccess und folgendem Inhalt zu versehen:

<FilesMatch .php>
Order Deny,Allow
Deny from All
</Files>

Hier noch einmal das gesamte Gist:

Achtung: Nur verwenden, wenn du wirklich weißt, was du tust und die Anweisungen verstehst sowie die Konsequenzen einschätzen kannst!

Du hast noch mehr Ideen, wie WordPress per .htaccess abgesichert werden kann? Dann ab in die Kommentare damit. Entweder hier im Blog oder direkt am Gist. Danke schön!

15 Antworten auf Mehr Sicherheit für WordPress per htaccess

  1. Danke für die Zusammenstellung.

    Ich nutze in der .htaccess noch

    ======

    RewriteCond %{REQUEST_URI} ^/$
    RewriteCond %{QUERY_STRING} ^/?author=([0-9]*)
    RewriteRule ^(.*)$ http://www.example.com/some-real-dir/? [L,R=301]

    ======

    Damit wird das Ausspähen des Autors(?author=[Zahl]) verhindert.

    Quelle und mehr: https://wordpress.org/support/topic/author1-2-3-how-to-stop-it

    Woody

    • Da der Username nicht als geheim einzustufen ist (er wird womöglich an anderer Stelle preisgegeben), habe ich das bisher nicht ergänzt. Ist aber eine schöne Möglichkeit das zu unterbinden.

  2. Vielen Dank für Deine tolle Liste.

  3. Der letzte Schnipsel ab „# Disable PHP execution in /uploads“ funktioniert so leider überhaupt nicht sondern führt zu unendlichen Redirects. Aus meinem Errorlog:

    „[Tue Mar 08 21:36:47 2016] [error] [client x.x.x.x] Request exceeded the limit of 10 internal redirects due to probable configuration error. Use ‚LimitInternalRecursion‘ to increase the limit if necessary. Use ‚LogLevel debug‘ to get a backtrace.“

    Sicher, PHP-Skripte in /wp-content/uploads werden so nicht ausgeführt, aber richtig ist das nicht – und außerdem lückenhaft. Der PHP-Handler wird durch folgende Konfiguration in der php5.conf bei Debian aktiviert:

    SetHandler application/x-httpd-php

    Entweder man packt dann diesen Teil in eine .htacces in /wp-content/uploads und ersetzt SetHandler durch Order-Direktiven. Noch sicherer ist es, dass direkt in die Konfiguration vom Vhost zu packen:

    Order deny,allow
    Deny from all

  4. man kann auch noch eventuell nicht erwünschte Request-Methoden verbieten

    RewriteCond %{REQUEST_METHOD} ^(TRACK|DELETE|TRACE) [NC]
    RewriteRule ^.* – [F]

    (Quelle: Plugin iThemes Security)

  5. Hi Torsten,

    was hältst Du von folgender Erweiterung?

    RewriteEngine On
    RewriteCond %{HTTP_HOST} ^www\.your-domain\.com
    RewriteRule (.*) http://your-domain.com/$1 [R=301,L]

    Beste Grüße,
    Phil

  6. Die Zeile

    Header set Content-Security-Policy „default-src ’self‘; img-src ’self‘ http: https: *.gravatar.com;“

    führte in einer WP-Installation dazu, dass die Javascripte im Backend vom Firefox und vom Chrome blockiert wurden und das Backend zum Teil nicht zu gebrauchen war. Ich habe sie herausgenommen. Alles andere ist fehlerfrei.

    • Ja, ich sollte die Zeile auskommentieren und auf die doch viel kompliziertere, individuelle Einrichtung hinweisen. Am besten schreibe ich dazu nochmal einen eigenen Artikel. Im Artikel weise ich ja bereits auf solche Gefahren hin.

    • Wobei die Zeile nur besagt, dass standardmäßig Elemente nur vom eigenen Server erlaubt sind, bei Bildern jedoch zusätzlich noch der Gravatar-Server. Das sollte bei einer Standardinstallation also nicht zu blockierten Skripten führen. Müsste man sich im Einzelfall anschauen, woran das lag. Die Möglichkeit per Content-Security-Policy-Report-Only: zu testen habe ich ja im Artikel schon erwähnt.

  7. Hallo,
    ich habe die Anleitung befolgt (und noch andere Anleitungen im Internet, über die ich so im Laufe der Zeit gestolpert bin). Nun will ich in einigen Monaten meine Seite umziehen (eine Ebene hoch, von /WP auf /). Klingt total simpel, wollte ich aber erst mal testen bei einem Umzug von /WP auf /WP5. Dazu Duplicator (Pro) besorgt, aber (neben anderen Fehlern, die ein nachträglichen Export der alten DB und Import in die neue nötig machten): Alles eigtl. ok, aber: Duplicator erstellt eine neue, reine .htaccess auf der Root-Ebene, in die sich gleich ReallySimpleSSL einträgt. WP Rocket und Secupress meckern aber nun, sie hätten keinen Schreibzugriff auf .htaccess. (trotz 644, sogar 777 mal ausprobiert). Nun kommt der Artikel ins Spiel: Wie könnte ich bei all den Sicherheitsmaßnahmen mir selbst ein Eigentor geschossen haben? Ändern der wp-config.php (Einträge von Rocket und Secupress raus und Fileedit am Ende raus) nützen auch nichts, deaktivieren von Rocket scheitert am fehlenden Schreibrecht auf .htaccess usw.
    Hilfe 🙂

  8. Pingback: Bessere Sicherheit für WordPress mit sicheren Server-Headern | Kau-Boys

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert