PHP/Anwendung und Praxis/Formulardaten serverseitig auswerten

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Sie können Webformulare benutzerfreundlicher gestalten, indem Sie sie schon clientseitig validieren und so Benutzer frühzeitig auf Eingabefehler hinweisen. Trotzdem müssen alle Eingaben serverseitig noch einmal geprüft werden, um zu gewährleisten, dass die Daten für eine spätere Verarbeitung geeignet sind und um die Gefahr von XSS-Angriffen auszuschließen.

Dieser Artikel versteht sich als Ergänzung zum Grundlagenartikel zur Erstellung von Formularen in HTML. Wie auch dort werden zwei Anwendungsfälle beispielhaft vorgestellt:

  1. einfache Suche
  2. Bestellformular


Inhaltsverzeichnis

[Bearbeiten] GET und POST empfangen in $_GET und $_POST

Um die mit einem Formular versandten Daten mit PHP auszuwerten, muss man verstehen, dass PHP bereits eine Infrastruktur zum Empfang solcher Daten bereitstellt. Da Browser gegenwärtig Formulardaten nur mit den HTTP-Methoden GET und POST versenden, finden sich in PHP so empfangene Daten in den superglobalen Variablen $_GET und $_POST. In diesen superglobalen Variablen sind die Schlüssel-Wert-Paare in Form eines assoziativen Arrays abgelegt.

Information

In JavaScript ist ein Array eine Feldvariable, deren Werte mit Nummern versehen sind. Die Schlüssel sind in JavaScript also Ganzzahlen. In PHP können die Schlüssel alphanumerisch, also (fast!) beliebige Strings, sein. Arrays in PHP, bei denen die Schlüssel nicht nur aus Ganzzahlen bestehen, werden assoziative Arrays genannt.


Wenn im action-Attribut bereits ein URL-Parameter notiert steht, das Formular aber mittels der POST-Methode versandt werden soll, so finden sich auf PHP-Seite sowohl in $_GET, als auch in $_POST Schlüssel mit Daten. Es ist allerdings nicht besonders ratsam, die HTTP-Methoden auf diese Art zu vermischen.

[Bearbeiten] Wann ist GET oder POST zu verwenden?

[Bearbeiten] Regeln

Die Wahl der Übertragungsmethode hängt, will man es richtig machen, nicht etwa von der Präferenz des Programmierers ab, sondern folgt eigentlich ganz einfachen Regeln:

  1. Werden durch den Request lediglich andere Daten als Antwort empfangen, so ist die GET-Methode die richtige Wahl.
  2. Werden durch den Request Daten auf dem Server verändert, ist die POST-Methode die richtige Wahl.
  3. Werden Daten für Logins, insbesondere Passwörter übermittelt, dann ist nur POST die einzig richtige Wahl.

[Bearbeiten] Gründe

Die Gründe sind ebenso einfach wie die Regeln. Nehmen wir zuerst GET:

Es ist bei der GET-Methode unerheblich, ob Sie tatsächlich das Formular eines Browsers verwendet haben, oder ob sie die URL http://example.org/search.php?search=SELFHTML+Wiki als Link angeklickt oder als Bookmark aufgerufen haben. Aus serverseitiger Sicht ist das nicht nur für PHP stets das Selbe: Mit der Methode GET gesendete Formulardaten werden in URL-Parameter verwandelt und stehen dann auf dem Server in $_GET zur Verfügung.

Also würde der Browser aus einem Formular mit <input type="search" name="search"> etwas wie https://example.com/search.php?search=foo machen. Diese Abfrage kann zum einen als Link dargestellt und durch Proxi-Server oder den Browser gecached oder vom Benutzer als Bookmark gespeichert werden. Außerdem gibt es mit mod_rewrite (bei Apache) eine Möglichkeit, diesen URI z.b. als https://example.com/search/foo zu notieren, was in vielen Situationen vorteilhaft ist.

Nachteilhaft wäre es aber, GET in Abfragen zu verwenden, welche zu einer Änderung von auf dem Server gespeicherten Inhalten führen.

Nehmen wir an, der "Server" wäre etwas wie eine Fritzbox und eine URL wie ...

Beispiel
http://fritz.box/setPassword?newPassword=foobar
(Natürlich sind die Hersteller der Fritzbox nicht so dumm, eine solchen Angriff zuzulassen!)

... würde zu einer Änderung des in der Fritzbox gespeicherten Passwortes führen. Dann könnte man einfach ein paar Millionen Mails aussenden, welche einen Link enthalten und dann mal sehen, wie viele zufällig an der Fritzbox angemeldet waren und auf den Link geklickt haben. Dann könnte sich nämlich der Angreifer mit dem neuen Passwort anmelden und der Besitzer weiß nicht so recht warum.

Freilich ginge das auch gezielter, z.B. bei einem Bekannten, von dem man weiß, dass der eine Fritzbox hat.

Ein weiterer Umstand ist, dass die als GET-Paremeter gesendeten Daten in Logfiles enthalten sind. Programmierer mit Verstand werden Passwörter niemals im Klartext ablegen. Sendet man jetzt aber per GET

https://example.com/login?user=foo&passwd=bar

dann steht das Passwort und der Benutzername in den Logfiles. Der Angreifer, der diese Logfiles erbeutet, kann nicht mehr vor Lachen über die Dummheit des Programmierers, der sehr schön gezeigt hat, was in Sicherheitsfragen als "Bypass" bezeichnet wird: Aufwendig gehashte Passwörter in der Datenbank, verschlüsselte Übertragung und dann der Klartext im Logfile. Vermutlich macht der Ihre Firma in einer Weise berühmt, welche Sie für "ganz ungut" halten.

Ebenso könnte eine Datenänderung durch einen Link dazu führen, dass z.B. in einem Webshop immer wieder ein Artikel als "nicht verfügbar" gekennzeichnet wird. Z.B. Weil ein Mitarbeiter unwissentlich etwa eine solche URL als Favorit gespeichert hat:

https://example.com/angebote_verwalten.php?isbn=123456789&verfuegbar=0.

Wird POST als Methode verwendet und auch erwartet (weshalb z.B. in PHP eine Verwendung von $_REQUEST statt $_POST oder $_GET stets sehr gute Gründe haben und allenfalls nur an Stelle von $_GET stattfinden sollte) dann kann hier kein Schaden entstehen.

[Bearbeiten] Beispiel einer seltenen Ausnahme

Allerdings kann es durchaus - selten - Umstände geben, welche GET als die zu wählende Methode erscheinen lassen. Nehmen wir mal an, der Inhaber eines Geschäftes will seine An- oder Abwesenheit signalisieren:

https://example.com/kontakt.php?ChefIstDa=0 bzw. https://example.com/kontakt.php?ChefIstDa=1

könnte hier eine Variable für eine "Ampel" setzen. Das wäre in einem solchen Fall wohl auch im Hinblick auf die Folgen einer Fehlbedienung und bei einer Absicherung "noch vertretbar". Nur muss der Inhaber dann auch sehr diszipliniert handeln. Also brav den passenden Favorit bzw. Link aufrufen wenn er kommt oder geht. Zumindest die Aktivierung seiner Anwesenheitsnachricht könnte man automatisieren in dem die URL durch ein automatisch gestartetes Programm bzw. Skript aufgerufen wird sobald er sich an seinem Rechner anmeldet.

[Bearbeiten] Beispiele:

[Bearbeiten] einfache Suche

Beispiel: Suche mit GET
<form action="http://example.org/search.php">
  <p>
    <label>Suchbegriff <input name="search"></label>
    <button>finden</button>
  </p>
</form>
Nehmen wir an Sie geben im Textfeld "SELFHTML Wiki" ein. Dieses Formular wird beim Versenden dieses Datum mit dem Schlüssel search verschicken. Da die GET-Methode (default) gewählt wurde, wird der Browser folgende Zielseite ansteuern: http://example.org/search.php?search=SELFHTML+Wiki. Der Name "search" des URL-Parameters search entspricht dem Wert im name-Attribut des Eingabeelements.
<?php
if (array_key_exists('search', $_GET)) {
  tue_was_mit($_GET['search']);
}
?>
Das PHP-Script prüft, ob in dem superglobalen Array $_GET ein Schlüssel namens search enthalten ist. Ist das der Fall, wird eine (in diesem Beispiel nicht definierte) Funktion namens tu_was_mit aufgerufen, die den empfangenen Suchbegriff, den Wert zum Array-Schlüssel search, übergeben bekommt. Das Pluszeichen entsteht dadurch, dass der Browser das Leerzeichen zwischen "SELFHTML" und "Wiki" für den URL-Kontext passend maskieren muss. Der Wert von $_GET['search'] enthält kein Pluszeichen mehr, sondern stattdessen wieder das Leerzeichen.


[Bearbeiten] Bestellformular mit komplexeren Eingabemöglichkeiten

Beispiel: komplexere Eingabemöglichkeiten
<form action="http://example.org/order.php">
  <fieldset>
    <legend>Ware</legend>
    <ul>
      <li>
          <label for="article-1">Plüsch-Teddybär</label>
          <input id="article-1" name="article-1" type="number" value="0">
      </li>
      <li>
          <label for="article-2">Sofakissen "Sonnenblume"</label>
          <input id="article-2" name="article-2" type="number" value="0">
      </li>
    </ul>
  </fieldset>
  <fieldset>
    <legend>Bezahlung</legend>
    <p>
      <label for="payment">Art der Bezahlung</label>
      <select id="payment" name="payment">
        <option value="ae">American Express Card</option>
        <option value="master">MasterCard</option>
        <option value="visa">VISA</option>
        <option value="prepayment">Vorkasse</option>
      </select>
    </p>
    <p>
      <input id="email-receipt" name="email-receipt" type="checkbox">
      <label for="email-receipt">Quittung per E-Mail</label>
      <label for="email">an folgende Adresse</label>
      <input id="email" name="email" type="email">
    </p>
  </fieldset>
  <fieldset>
    <legend>Versand</legend>
    <dl>
      <dt>Lieferanschrift</dt>
      <dd>
          <ul>
            <li>
              <label for="recipient-name">Name</label>
              <input id="recipient-name" name="recipient-name">
            </li>
            <li>
              <label for="recipient-address">Anschrift</label>
              <input id="recipient-address" name="recipient-address">
            </li>
            <li>
              <label for="recipient-zip">PLZ</label>
              <input id="recipient-zip" name="recipient-zip">
              <label for="recipient-city">Ort</label>
              <input id="recipient-city" name="recipient-city">
            </li>
          </ul>
      </dd>
      <dt>Rechnungsanschrift</dt>
      <dd>
          <ul>
            <li>
              <label for="buyer-name">Name</label>
              <input id="buyer-name" name="buyer-name">
            </li>
            <li>
              <label for="buyer-address">Anschrift</label>
              <input id="buyer-address" name="buyer-address">
            </li>
            <li>
              <label for="buyer-zip">PLZ</label>
              <input id="buyer-zip" name="buyer-zip">
              <label for="buyer-city">Ort</label>
              <input id="buyer-city" name="buyer-city">
            </li>
          </ul>
      </dd>
    </dl>
  </fieldset>
  <fieldset>
    <legend>Zusatz</legend>
    <p>
      <label for="message">Ihre Nachricht an uns</label>
      <textarea id="message"></textarea>
    </p>
  </fieldset>
  <p><button>jetzt Kostenpflichtig bestellen</button></p>
</form>
Alle Eingabeelemente haben ein name-Attribut, damit ihr Inhalt als Schlüssel-Wert-Paar übertragen werden kann. Die einzige Besonderheit hier ist tatsächlich die Checkbox für die Quittung per E-Mail. Bei Checkboxen muss man serverseitig auf das Vorhandensein des Schlüssels prüfen, da bei nicht gesetztem Häkchen der Name der Checkbox unter den Schlüsseln fehlt.
// mögliche Zahlungsarten
$payment = array(
  'ae' => 'American Express Card',
  'master' => 'MasterCard',
  'visa' => 'VISA',
  'prepayment' => 'Vorkasse'
);
 
// Voreinstellungen für eine Bestellung
$order = array(
  'article-1' => 0,
  'article-2' => 0,
  'payment' => 'prepayment',
  'email-receipt' => false,
  'email' => '',
  'recipient-name' => '',
  'recipient-address' => '',
  'recipient-zip' => '',
  'recipient-city' => '',
  'buyer-name' => '',
  'buyer-address' => '',
  'buyer-zip' => '',
  'buyer-city' => '',
  'message' => ''
);
 
// für alle Schlüssel einer möglichen Bestellung
// nach geposteten Daten suchen
foreach ($order as $key => $value) {
 
  if (array_key_exists($key, $_POST)) {
 
    // nach Schlüssel differenzieren
    switch ($key) {
 
      // Bestellmenge: nur numerische Werte akzeptieren!
      case 'article-1':
      case 'article-2':
        $order[$key] = abs($_POST[$key]);
      break;
 
      // Zahlungsart: nur definierte Werte akzeptieren!
      case 'payment':
        if (array_key_exists($_POST[$key], $payment)) {
          $order[$key] = $_POST[$key];
        }
      break;
 
      // checkbox! Sein oder Nichtsein
      case 'email-receipt':
        $order[$key] = true;
      break;
 
      default:
        $order[$key] = $_POST[$key];
    }
  }
}
Nachdem die Datenstruktur einer Bestellung in der Variable $order erstellt und mit Voreinstellungen befüllt wurde, werden nun die Schlüssel dieses Arrays mit den Schlüsseln in $_POST abgeglichen. Bei Checkboxen will man eine "ja"/"nein"-Unterscheidung (true/false), daher ist hier nur wichtig, ob der Schlüssel in $_POST überhaupt vorhanden ist. Bei Bestellmenge und Zahlungsmethode müssen die Eingaben daraufhin geprüft werden, ob sie so erlaubt sind, bevor sie akzeptiert werden.
Beachten Sie: Getreu dem Motto "alle Benutzereingaben sind böse!" werden die Eingaben auf erlaubte Werte hin geprüft. Außerdem kümmert sich unser Script nur um Schlüssel, die auch erwartet werden, und kann durch unsinnige Schlüssel-Wert-Paare nicht angegriffen werden.

[Bearbeiten] Siehe auch

[Bearbeiten] HTML

[Bearbeiten] PHP

[Bearbeiten] Referenzen

Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Index
Mitmachen
Werkzeuge
Spenden
SELFHTML