Sicher ? Sicherlich ! WordPress absichern.

Bei einem der Themes das ich nutzen mußte gab es Probleme zwischen zwei installierten Plugins. Es stellte sich heraus, das eines der involvierten Plugins ein sogenanntes Security-Plugin war. Im Ergebnis landete die Sicherheit im Eimer und mir wurde zum einen nochmal die Monströsität dieses Sicherheitsplugins verdeutlicht und zum anderen war ich not amused welche Schreibrechte dieses Plugin für sich beanspruchte.
Ein Grund mich ein wenig auf Spurensuche zu begeben und versuchen zu Fuss das zu tun, was diese besonderen Sicherheits-Plugins sonst erledigen.
Ein Problem das auch hierbei immer wieder auftritt: Ein Snippet das heute aktuell ist und funktioniert, ist morgen möglicherwiese überholt und hilft schon nichts mehr. Eine ewige Jagd.
Außerdem: suche ein wenig im Netz nach „Snake-Oil“ oder „Security through obscurity“.

WP Installation – Routine

Einige Dingelchens sind bereits bei einer routinierten Installation abzuspulen:
Authentication Unique Keys, achte darauf das bei der Installation kein Standard-User „admin“ angelegt wird, nach der Installation nicht benötigte Themes und Plugins löschen.
Tabellen-Präfix ändern: Viele WordPresslerInnen kommen zu dem Schluss: The database table prefix is not a security feature… It doesn’t matter what the prefix is … So why is the prefix a configuration option at all? It allows you to run more than one installation in the same database.
Falls es doch mal notwendig sein sollte ändere zwecks Verschleierung nachträglich den DB-Päfix, wenn er noch „wp_“ lautet. Benutze dafür ein Plugin oder tätige es per Hand: in der config.php den Präfix ändern, Tabellen umbenennen und vergesse nicht: Ein Update der Felder in den Tabellen „usermeta“ und „option“.
Sicheres Passwort: Leider für viele NutzerInnen keine Routine. Empfohlen wird eine länge von 12 Zeichen mit Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen.

Login error verschleiern

Bei fehlerhaftem Login ins Backend meldet WordPress sinngemäß „Passwort falsch“ oder „Benutzername falsch“. Entferne diesen unnötigen Hinweis über die function.php (Sicherheitsfaktor: 0, Spaßfaktor: 10):

add_filter('login_errors', create_function('$a', "return '<b>Oops!</b> its wrong';"));

Versionsnummern-Ausgabe vernebeln

Ein hoffnungsloses Unterfangen, eine schöne Spielerei: An scheinbar unendlichen Stellen läßt sich die aktuelle Versionsnummer auslesen.
Bei automatisierten Angriffen (und das dürfte die Regel sein)  spielt die Frage welche WP-Version läuft keine Rolle. Die entscheidende Frage lautet: ist das System angreifbar.
Ein kleines todo für die function.php um die Versionsnummern-Ausgabe aus dem WP-Head zu entfernen:

remove_action('wp_head', 'wp_generator');

Bei älteren WP-Versionen wurde die Versionsnummer in your-domain.xyz/license.txt angezeigt. Willst du grundsätzlich das Auslesen blockieren siehe dazu „Zugriffschutz .htaccess“.
Über weitere Links (zu finden im Quelltext des WP-Header) erhälst du ebenfalls Auskunft über die WP-Version, z.B.: deine-domain.de/feed und deine-domain.de/comments/feed und deine-domain.de/homepage/feed .
Wenn du diese Funktionalitäten nicht benötigst, schalte alle ab (bezüglich /feed auch interessant im Zusammenhang mit „Benutzernamen vernebeln“). Folgendes Snippet schickt die NutzerInnen beim jeweiligen Aufruf zur Startseite:

function disable_feeds() {
	wp_redirect( home_url() );
	die;
}

// Disable global RSS, RDF & Atom feeds.
add_action( 'do_feed',      'disable_feeds', -1 );
add_action( 'do_feed_rdf',  'disable_feeds', -1 );
add_action( 'do_feed_rss',  'disable_feeds', -1 );
add_action( 'do_feed_rss2', 'disable_feeds', -1 );
add_action( 'do_feed_atom', 'disable_feeds', -1 );

// Disable comment feeds.
add_action( 'do_feed_rss2_comments', 'disable_feeds', -1 );
add_action( 'do_feed_atom_comments', 'disable_feeds', -1 );

// Prevent feed links from being inserted in the <head> of the page.
add_action( 'feed_links_show_posts_feed',    '__return_false', -1 );
add_action( 'feed_links_show_comments_feed', '__return_false', -1 );
remove_action( 'wp_head', 'feed_links',       2 );
remove_action( 'wp_head', 'feed_links_extra', 3 );

Trotz all dieser Handgriffe finden sich im Seitenquelltext noch weitere Hinweise wie dieser
(hier auf eine ältere WP-Version):

'.../wp-includes/js/wp-embed.min.js?ver=5.3.2'

Oder du hast direkten Zugriff auf die /wp-login.php, dann siehst du im Header des Quelltextes den Hinweis:

'...wp-includes/css/dashicons.min.css?ver=5.4'
'...wp-includes/css/buttons.min.css?ver=5.4'

Directory Browsing prüfen

Das Directory Browsing ist bei den meisten Servern eh abgeschaltet, aber schau nach und verhindere es ggfs. In die htaccess kommen folgende zwei Zeilen:

# Prevent Directory Listings
Options -Indexes

Benutzer-Accounts vernebeln

Bei etwas genauerer Betrachtung erweist sich der Versuch die WP-BenutzerInnen zu verschleieren als ähnlich umfangreiches Unterfangen wie das Verschleieren der Versionsnummer.
Wordpress schreibt dazu: Das WordPress-Projekt betrachtet Benutzernamen oder Benutzer-IDs nicht als private oder sichere Informationen. Ein Benutzername ist Teil Ihrer Online-Identität. Es soll identifizieren, nicht verifizieren …. Die Verifizierung ist die Aufgabe des Passworts.
Nutze sichere Passwörter und verriegel die wp-login via htaccess.

Benutzer-Accounts vernebeln: htaccess

Zur Erkennung des Benutzer-Accounts gebe in die Adresszeile des Browsers ein: meine-domain.com/?author=1. So könntest du mal 10 IDs durchprobieren und die dazugehörigen Nutzernamen herausfinden. Über folgenden Eintrag in die htaccess ließe sich das unterbinden:

RewriteCond %{REQUEST_URI} ^/$
RewriteCond %{QUERY_STRING} ^/?author=([0-9]*)
RewriteRule ^(.*)$ http://www.geh-weg.da/irgendwohin/? [L,R=301]

Benutzer-Accounts vernebeln: öffentlicher Name

Gebe dir in deinem WP-Profil einen Spitznamen der sich von deinem Benutzernamen unterscheidet. Wähle diesen Spitznamen dann als „Öffentlicher Name“.
Rufts du die url „deine-domain.de/feed/“ auf, erhälst unter Umständen ebenfalls Information über WP-Nutzerinnen. Unter dc:creator … CDATA[hier_steht_der_autor] …. sollte nicht dein Benutzername sondern dein „Öffentlicher Name“ stehen.

Benutzer-Accounts vernebeln: WP REST API prüfen

Prüfe ob die REST API (REpresentational State Transfer) Schnittstelle abgeschaltet ist. Die REST-API für deine Seite ist aufzurufen über meinedomain.xyz/wp-json/
Weniger wünschenswert: Über die REST-API lassen sich gezielt alle erstellten WP-BenutzerInnen abfragen (in diesem Falle: query endpoint user): https://www.deine-domain.de/wp-json/wp/v2/users
Du könntest eine nicht benötigte REST API komplett abschalten. Das Problemchen: Es gibt Themes und Plugins die bei abgeschalteter REST-API nicht mehr bzw. nicht mehr korrekt arbeiten.
Für manche Themes läßt sich das umgehen indem die REST API nur für nicht eingeloggte User abgeschaltet wird. Beiträge lassen sich dann wieder anlegen und bearbeiten Ein Beispiel wäre:

function disable_wp_rest_api($access) {
 if ( ! is_user_logged_in() ) {
   return new WP_Error( 'rest_cannot_access', array( 'status' => rest_authorization_required_code() ) );
   return $access;
 }
}
add_filter( 'rest_authentication_errors','disable_wp_rest_api');

Das obige Snippet führt jedoch auch zu lustigen Verwirrungen, zum Beispiel mit dem Contact Form 7 Plugin. Ist die NutzerInn eingeloggt und testet im Frontend das Formular, so sendet es korrekt. Ist die NutzerInn ausgeloggt , sendet es nicht mehr. Teste folgendes Snippet:

add_filter( 'rest_endpoints', function( $endpoints ){
 if( ! is_user_logged_in() ) {
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
 }
});

Du kannst es auch mit verschiedenen Plugins ausprobieren:  Disable WP REST API oder  Disable REST API
Prüfe ob deine genutztne SEO Plugins etwas wie „script type=“application/ld+json“ ….“ in den Header legen. Auch dort finden sich Benutzerinnen Informationen.
Auf jeden Fall: Teste immer deine Theme Funktionalitäten und teste immer die (entsprechenden) Plugin Funktionalitäten durch.

wp-login schützen | htaccess

Ein Top-Act, der in Regel von sehr bequemen WP-NutzerInnen zusätzliche Mühen erfordert: Sichere die wp-login.php mit einer htaccess und htpasswd.
Beachte: Die login.php mit einer htaccess zu sichern erzwingt doppelte PW-Eingabe bei passwortgeschützten Beiträgen.

Login schützen | plugin

Ob des Sinns immer wieder diskussionswürdig ist der Einsatz von Plugins wie Login LockDown  oder Limit Login Attempts. Diese Plugins sorgen dafür, daß nach mehreren falschen Login-Versuchen im WP-Backend die entsprechende IP gesperrt wird.
Login LockDown legt zwei Tabellen in der Datenbank an. Wenn du Zeit hast kannst du dir besonders nervige IP-Bereiche aus dem protokolliertem Pool holen (und bei Bedarf sperren). Und wenn du nachschaust wirst du feststellen, das eine der Tabellen mit der Zeit recht aufgeblasen wird: Leeren.

wp-login schützen | IP sperren

Falls es klar und übersichtlich ist welche NutzerInnen das WP-Backend nutzen: Der WP-Admin Zugang ließe sich via htaccess auf bestimmte IP-Adressen (IP-Ranges) beschränken.

XML-RPC-Schnittstelle abschalten

Die XML-RPC-Schnittstelle ist ein Tool um mittels Desktop– bzw. Smartphone-Apps die Website / Artikel zu verwalten. Angeblich basieren viele Funktionen des Jetpack-Plugins auf dieser Schnittstelle. Wer zum Beispiel über ein externes Tool via XML-RPC-Schnittstelle zugreifen will, durchläuft eine zusätzliche Authentifizierung (RPC = Remote Procedure Call / entfernte Funktionsaufrufe, strukturiert mit xml). Deaktiviere mit folgendem Code in der htaccess

#XML-RPC Schnittstelle abschalten
<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>

Zugriffschutz /wp-includes

Das wp-include Verzeichnis vor Zugriffen von aussen schützen.

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

Mit diesem Schnippsel gibts Probleme bei multi sites. Deshalb entferne: RewriteRule ^wp-includes/[^/]+.php$ – [F,L]

Zugriffschutz /uploads

To ensure no .php files can be executed, create a .htaccess file in /wp-content/uploads containing following code (mittels Files oder FilesMatch):

<Files *.php>
deny from all
</Files>
Oder
<FilesMatch .php>
Order Deny,Allow
Deny from All
</Files>
Oder
# Ab Apache 2.4
# Disable direct access of any *.php files in /wp_content/uploads folder
<FilesMatch .php$>
Require all denied
</FilesMatch>

Zugriffschutz .htaccess

Zugriffe über die htaccess regeln – einzeln oder im Pack:

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

Zugriffschutz CHMOD

Nicht nur der config.php reichen im Idealfall 0400-Rechte, also nur Leserechte und nur für den Besitzer (Nicht bei allen Anbietern möglich.). Auch die htaccess muß nur lesbar sein. Hast du die volle Kontrolle über das Projekt kannst du Template Dateien oder auch eigene Plugins auf nur lesbar setzen.
Die header.php, footer.php, index.php und functions.php sind mindestens dafür interessant.

Zugriffschutz WP Backend

Wenn verhindert werden soll, daß im WP-Backend Themes und Plugins geändert werden können, schreibe in die config.php:

define('DISALLOW_FILE_EDIT', true);

Verzeichnis in work: sperren

In einem laufenden Projekt eine Seite vorübergehend via htaccess schützen, suche im Netz nach: Verzeichnisschutz für Rewrite Pfade.

Fehlermeldungen unterdrücken

Schraubst du in deinen php-files Dinge fehlerhaft zusammen erscheinen nicht nur einfache Fehlermeldungen, sondern es werden gleich die Pfade präsentiert. Was auf der Entwicklungsumgebung sinnvoll ist, erscheint auf der Live-Seite als peinlich. Teste verschiedene Möglichkeiten dies zu unterdrücken.

In htaccess: php_flag display_errors off
In config.php: define('WP_DEBUG', false);
In config.php: define('WP_DEBUG_DISPLAY', false);
In config.php: ini_set ('display_errors', 0);

Klein und kompakt

Begrenze die Anzahl der NutzerInnen die Zugriff auf das Backend haben. Es gibt viele Projekte bei denen sich im Laufe der Zeit überflüssige Nutzer*Innen ansammeln, entferne diese Karteileichen. Und versuche in Absprache mit dem Kunden die eingetragenen NutzerInnen mit Adminrechten zu reduzieren/begrenzen.

Reduziere die Rechte der NutzerInnen: Um Seiten zu editieren sind keine Adminrechte notwendig. Lege dir ggfs. einen zusätzlichen Nutzer für solche Aufgaben an. Gerade wenn der Zugang unsicher erscheint (von unterwegs ?): benutze nicht den Adminzugang.

Halte die Datenbank sauber: Zur Optimierung und Bereinigung der WordPress Datenbank suche ein Plugin wie WP-Optimize oder WP Clean UP. Wenn Kunden erste Übungen mit ihrem 59-USD-Theme anstellen und du die ersten Datenbankbackups anlegst und du dich über 30MB große Datenbanken wunderst, dann prüfe in der config.php ob die Anzahl der zu speichernden Versionen wirklich so hoch sein muß: define( ‚WP_POST_REVISIONS‘, 15 );
Ganz abschalten: define(‚WP_POST_REVISIONS‘, FALSE);

Der WordPress-header lädt unzählige Funktionen. Zum abschalten suche im Netz nach Dingen wie: wordpress header bereinigen. Oder nach: Disable the emoji’s

Cookie Checker

cookieserve.com

Links

Inspirierend (kuketz-blog.de):
WordPress absichern
Weitere Einstellungen in der .htaccess (aka the 6G Blacklist):
perishablepress.com/6g/
Wordpress empfiehlt:
wordpress.org/support/article/hardening-wordpress/
Ein wenig zum Thema Datenschutz, google-fonts und gravatare im WP-Backend:
journalismus-plus.de/generieren/spionageprogramme-in-wordpress-deaktivieren
robots.txt und Bots … nett zu lesen:
perishablepress.com/wordpress-robots-rules/
perishablepress.com/blackhole-bad-bots/

update: 20. Apr 2020