WordPress mit verschiedenen Datenbanknutzern absichern

WordPress ist nicht gerade ein Stück Software, das für sein herausragendes Sicherheitskonzept bekannt ist. Viele Dinge muss man erst mühsam im Alleingang nachbessern.

So hat es mich schon immer gestört, dass Nutzer, die sich meinen Blog ansehen, im Hintergrund den gleichen Datenbanknutzer verwenden, wie ich als Administrator, der die Seite konfiguriert und neue Bloginhalte bereitstellt. Wenn es möglich wäre, wenigstens für diese zwei stark gegenteiligen Nutzungsszenarien verschiedene Datenbanknutzer zu verwenden, wäre schon viel erreicht. Aus diesem Grund habe ich mich heute einmal hingesetzt und genau das implementiert. Alle Änderungen, die ich vornehmen mussten, sind so konzipiert, dass sie ein Update der WordPress-Installation überleben. Das heißt, dass keine Core-Files angerührt werden müssen. 🙂

Das größte Problem, das zu lösen war, ist, dass der Datenbanknutzer schon relativ früh beim Einlesen der "wp-config.php" gewählt wird. Die Information, dass man eingeloggt ist, steht jedoch erst viel später zur Verfügung. Es musste also ein Weg gefunden werden, diese Information bereits früher bereitzustellen. Meine Lösung ist es, beim erfolgreichen Login einfach eine Session-Variable zu schreiben und diese nach einem Logout wieder zu löschen. Das erledige ich mit einem Plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php
  /*
    Plugin Name: LoginSession
    Plugin URI: https://weizenspr.eu/
    Description: Generates a reusable PHP session when logged in.
    Version: 0.1c3
    Author: Kevin Niehage
    Author URI: https://weizenspr.eu/

    this code is based on http://wordpress.org/support/topic/using-session-in-wordpress
  */


  /* STOP EDITING HERE IF YOU DO NOT KNOW WHAT YOU ARE DOING */

  define("LOGINSESSION_LOGGEDIN", "loginsession_loggedin");
  define("LOGINSESSION_STARTED",  "loginsession_started");

  function loginsession_start() {
    $started = false;

    // check if session already exists
// will work starting with PHP 5.4.0:
//    if (PHP_SESSION_NONE === session_status()) {
    if ("" === session_id()) {
      $started = session_start();
    }

    // session should exist now
// will work starting with PHP 5.4.0:
//    if (PHP_SESSION_ACTIVE === session_status()) {
    if ("" !== session_id()) {
      // prevent session fixation
      session_regenerate_id(true);

      $_SESSION[LOGINSESSION_LOGGEDIN] = true;
      $_SESSION[LOGINSESSION_STARTED]  = $started;
    }
  }

  function loginsession_stop() {
    // check if session still exists
// will work starting with PHP 5.4.0:
//    if (PHP_SESSION_ACTIVE === session_status()) {
    if ("" !== session_id()) {
      // prevent session fixation
      session_regenerate_id(true);

      // remove loggedin value from session
      if (isset($_SESSION[LOGINSESSION_LOGGEDIN])) {
        unset($_SESSION[LOGINSESSION_LOGGEDIN]);
      }

      // check if we started the session
      if (isset($_SESSION[LOGINSESSION_STARTED])) {
        if ($_SESSION[LOGINSESSION_STARTED]) {
          // destroy the session if we did
          session_unset();
          session_destroy();

          // unset the session cookie
          if (isset($_COOKIE[session_name()])) {
            $params = session_get_cookie_params();
            setcookie(session_name(), "", time()-3600, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);
          }
        } else {
          // remove started value from session if we did not
          unset($_SESSION[LOGINSESSION_STARTED]);
        }
      }
    }
  }

  add_action("wp_login",  "loginsession_start");
  add_action("wp_logout", "loginsession_stop");
?>

Anschließend bedarf es noch einer Änderung in der "wp-config.php". Dort werden statt einem Datenbanknutzer (bestehend aus DB_USER und DB_PASSWORD) nun zwei Datenbanknutzer definiert. Der mit den umfangreichen Rechten wird nur noch dann genutzt, wenn entweder der Wert "loginsession_loggedin" in der Session auf TRUE gesetzt ist oder wenn die Konstante "DOING_CRON" definiert ist. Das eine ist der Fall, wenn der WordPress-Nutzer eingeloggt ist, das andere ist der Fall, wenn das Script "wp-cron.php" ausgeführt wird:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$loginsession_evaluated = false;
// open the session if there is one
if ((isset($_COOKIE[session_name()])) || (isset($_GET[session_name()]))) {
  session_start();

  if ((isset($_SESSION["loginsession_loggedin"])) &&
      ($_SESSION["loginsession_loggedin"])) {
    $loginsession_evaluated = true;
  }
}

// select user dependent on loginsession
if (($loginsession_evaluated) || (defined("DOING_CRON"))) {
  define("DB_USER",     "<ADMIN USER>");
  define("DB_PASSWORD", "<ADMIN PASSWORD>");
} else {
  define("DB_USER",     "<GUEST USER>");
  define("DB_PASSWORD", "<GUEST PASSWORD>");
}

Damit das ganze ordentlich funktioniert, müsst ihr natürlich noch den Datenbanknutzer für eure Gäste anlegen. Dazu verbindet ihr euch mit eurer Datenbank (wahrscheinlich MySQL) und führt dort ein paar Befehle aus. Ich gehe davon aus, dass eure Datenbank "wordpress" heißt und ihr den Präfix "wp_" verwendet habt.

1
2
3
4
5
6
7
8
CREATE USER '<GUEST USER>'@'%' IDENTIFIED BY '<GUEST PASSWORD>';
GRANT SELECT ON wordpress.* TO '<GUEST USER>'@'%';
GRANT INSERT ON wordpress.wp_commentmeta TO '<GUEST USER>'@'%';
GRANT INSERT ON wordpress.wp_comments TO '<GUEST USER>'@'%';
GRANT UPDATE ON wordpress.wp_commentmeta TO '<GUEST USER>'@'%';
GRANT UPDATE ON wordpress.wp_comments TO '<GUEST USER>'@'%';
GRANT DELETE ON wordpress.wp_commentmeta TO '<GUEST USER>'@'%';
GRANT DELETE ON wordpress.wp_comments TO '<GUEST USER>'@'%';

Mit dieser Grundausrüstung können eure Gäste weiterhin alles lesen, jedoch nur noch Kommentare erstellen. Sie können hingegen keine Nutzeraccounts mehr manipulieren, keine Blogbeiträge mehr manipulieren und auch keine Konfigurationseinstellungen mehr manipulieren. So schnell habt ihr euren WordPress-Blog vor jeder Menge Zero-Day-Exploits abgesichert.

Mit diesen restriktiven Einstellungen funktioniert eins jedoch (offenbar) nicht mehr: das Verwenden des WordPress-Cronjobs. Dieser wird typischerweise regelmäßig durch die Besucher des Blogs gestartet. Das ganze Problem kann jedoch wie folgt umgangen werden. Zuerst einmal muss in die "wp-config.php" noch eine weitere Zeile aufgenommen werden:

1
define("DISABLE_WP_CRON", true);

Anschließend richtet ihr in eurem System einen echten Cronjob ein, der in regelmäßigen Abständen folgenden Befehl aufruft. Achtet darauf, dass der Cronjob seltener läuft, als ein PHP-Script bei euch laufen darf. Damit verhindert ihr Locksituationen.

1
wget -q -O - http://<BLOGURL>/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Je nachdem, was ihr sonst noch so für Plugins betreibt, kann es notwendig sein, dass ihr dem Account "<GUEST USER>" weitere Rechte in eurer Datenbank einräumt - zum Beispiel, wenn ihr ein Nutzertracking verwendet. Da dies jedoch davon abhängt, was ihr sonst noch alles so für Plugins verwendet, kann dies nicht Teil dieses Blogbeitrags sein. 😉

Zum Schluss noch etwas Rechtliches:
Der Autor dieses Programms haftet nicht für Schäden an Soft- oder Hardware oder Vermögensschäden, die durch das Benutzen des Programms entstehen, es sei denn, diese beruhen auf einem grob fahrlässigen oder vorsätzlichen Handeln des Autors, seiner Erfüllungsgehilfen oder seiner gesetzlichen Vertreter.

Absichernde Grüße, Kenny

3 Kommentare » Schreibe einen Kommentar

  1. Pingback: WordPress Blogschau - Was war los im Februar?

    • Häh? Wie kommst du denn darauf? Du müsstest eine Session auf meinem Server erzeugen und darin den Wert "loginsession_loggedin" hinterlegen können.

      Und wenn du das geschafft hast, bist du auf dem Sicherheitsniveau, das andere WordPress-Blogs von Hause aus haben.

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.