Studieren in Chemnitz. Wissen, was gut ist.






Sicheres Programmieren mit PHP (4)

SQL Injection

PHP eignet sich hervorragend, um mit WWW-Seiten Daten aus Datenbanken anzuzeigen oder zu ändern. Aber auch hier gilt es, Sicherheitsaspekte zu beachten, will man nicht böse Überraschungen erleben.

SQL Injection ist eine Technik, mit der böswillige Angreifer über WWW SQL-Kommandos erstellen oder existierende verändern, um versteckte Daten sichtbar zu machen, zu verändern oder zu löschen.

Betrachten wir wieder ein schlechtes Beispiel und ein Angriffs-Szenario:

   <?php 
     #Datenbankabfrage an Hand einer ID aus dem URL
     $id = $_REQUEST['id'];
     $result = mysql_query("SELECT * from tabelle WHERE id=$id");
     # Anzeige des Ergebnisse ...
   ?>

"Erwartet" wird ein URL der Form .../script.php?id=42, wodurch aus der Datenbank der Tabelleneintrag mit der id=42 gelesen wird. Was aber, wenn der URL so geschrieben wird: .../script.php?id=42+or+1=1
Die Pluszeichen wandelt der WWW-Server in Leerzeichen um, und der arglose mysql_query -Aufruf führt plötzlich dazu, dass alle (mglw. sensiblen) Daten ausgelesen werden:
SELECT * from tabelle WHERE id=42 or 1=1

Auch in diesen Fall erinnern wir uns an den Merksatz: Wir müssen alle Eingabewerte überprüfen, auch wenn das etwas Mühe macht. PHP hält dafür aber eine Menge an Möglichkeiten bereit.

Wird als Wert eine Integer-Zahl erwartet, so können Sie dies prüfen mit der Funktion is_numeric() oder Sie setzen den Typ mittels settype() explizit auf integer:

   <?php 
     #Datenbankabfrage an Hand einer ID aus dem URL
     $id = $_REQUEST['id'];
     settype($id, 'integer');   # Zwangsumwandlung in Zahl
     $result = mysql_query("SELECT * from tabelle WHERE id=$id");
     # Anzeige des Ergebnisse ...
     }
   ?>

Werden als externe Parameter Zeichenketten erwartet, gerät die Prüfung meist etwas aufwändiger. Hier ist eine Prüfung auf plausible Werte angebracht. Zumindest sollten Sie Zeichen kodieren, die für SQL eine Sonderbedeutung haben: Semikolon, einfacher und doppelter Anführungsstrich usw. Dafür bietet sich die PHP-Funktion addslashes() an.

   <?php 
     #Datenbankabfrage an Hand eines eingetippten Suchwortes
     $suchwort = $_REQUEST['wort'];

     # Entferne rigoros alle Semikolons
     $suchwort = ereg_replace(';', '', $suchwort);
     if (get_magic_quotes_gpc() == 0) {
        # Falls die System-PHP-Einstellungen es noch nicht automatisch tun,
        # kodiert die Funktion addslashes weitere SQL-Sonderzeichen:
        $suchwort = addslashes($suchwort);
     }
     $result = mysql_query("SELECT * from tabelle WHERE name like \"$suchwort\"");
     # Anzeige des Ergebnisse ...
     }
   ?>

Am besten ist es, Sie lehnen dubios erscheinende Abfragen von vornherein ab:

   <?php 
     #Datenbankabfrage an Hand eines eingetippten Suchwortes
     $suchwort = $_REQUEST['wort'];

     $laenge = strlen($suchwort);
     # Lehne zu kurze/lange oder ein Semikolon enthaltende Suchwörter ab:
     if ($laenge <= 0 || $laenge > 100 || ereg(';', $suchwort)) {
        print "Fehler im Suchwort ...";
        exit;
     }
   ?>

Die Programmier-Sorgfalt muss noch größer sein, wenn die Datenbank via PHP auch geändert werden soll. Bei INSERT- und UPDATE- Operationen muss sichergestellt werden, dass plausible Werte eingefügt werden. Besondere Vorsicht ist natürlich bei DELETE-Operationen usw. nötig.

Für größere PHP-Projekte mit Datenbank-Zugriff empfiehlt sich auch aus Sicherheitsaspekten die Einarbeitung in Datenbank-Abstraktionspakete, wie das PEAR-Paket MDB2 oder - ab PHP 5.1 - PDO (PHP Data Objects):

  • Große Unabhängigkeit von der zugrunde liegenden Datenbank
  • Objektorientierter Ansatz
  • Sicherheit: Vorgefertigte Abfragen (prepared statements) mit Platzhaltern, eingesetzte Werte werden automatisch sicher "geflüchtet"
<form action="d1.php">
  ID: <input name="id" size="4" /> 
  <input type="submit" value="Senden" />
 </form>
<?php
  if (! isset($_REQUEST['id'])) return;  # id nicht gesetzt: Schluss

  require_once("MDB2.php");       # PEAR MDB2 einbinden

  $db =  MDB2::factory('mysql://@www-db.tu-chemnitz.de/events');
# $db =  MDB2::factory('mysql://benutzername:passwort@server/datenbank');

  if (PEAR::isError($db)) { die ($db->getMessage()); }

# Vorbereiten mit Platzhaltern und Typ
  $db_q = $db->prepare('SELECT titel FROM event WHERE id=?', array('integer'));

# Ausführen
  $res = $db_q->execute($_REQUEST['id']);  # ziemlich ungefährlich
  if (PEAR::isError($res)) {    # Fehler?
      die($res->getMessage());
  }

#Ergebnisse abfragen
  while (($row = $res->fetchRow())) {
    echo 'Titel: ' . htmlspecialchars($row[0]) . "<br />\n";
  }
?>

Vor einer Abfrage von Daten muss zunächst die Verbindung zur Datenbank hergestellt werden, wozu neben Servernamen auch der Datenbank-Nutzer und das Passwort anzugeben ist, z.B. für MySQL:

   <?php 
     #Datenbank-Verbindung herstellen:
     # das @ vorm Funktionsaufruf verhindert die eingebaute Fehlermeldung
     $db = @mysql_connect('mysql.hrz.tu-chemnitz.de', 'dbnutzer', 'geheim')
           or die ("Keine Verbindung zur Datenbank möglich");
   ?>

Hier haben wir das Problem, dass das Passwort des Datenbank-Benutzers im PHP-Skript stehen muss. Das birgt natürlich eine Gefahr, wenn es in falsche Hände gelangt. Als Grundregel beachten Sie: Verwenden Sie einen Datenbank-Benutzer, dessen Rechte entsprechend Ihrer Anwendung limitiert sind. Wenn Sie via PHP-Skript nur SELECT-Anweisungen ausführen, verwenden Sie den DB-Nutzer, der nur leseberechtigt ist.

Mit einem speziellen Verfahren steht Web-Programmierern auf den zentralen Webservern der TU ein sicherer Weg zur Verfügung, solche Geheimnisse in Webanwendungen zu verwenden, ohne dass diese im zugänglichen Dateisystem abgelegt werden müssen.

Für Anwendungen und Projekte mit höheren Sicherheitsanforderungen sind dedizierte WWW-Server vorzusehen. Das URZ bietet dafür sog. PROWeb-Server an.

Weitere Hinweise: http://www.tu-chemnitz.de/docs/php/security.database.html