URL-Manipulationen verhindern

Heute mal ein Beitrag, der nicht ganz so sehr auf Performance (aber auch) sondern auf Sicherheit abzielt. Ganz nebenbei hat das auch wirtschaftlich Sinn. Aus aktuellem Anlass bei einer meiner Seiten bin ich auf eine Idee gekommen, wie man URL-Manipulationen zwecks SQL-Injection effektiv verhindern kann. Getreu nach dem Motto „All incoming data is evil“…

Es gibt verschiedene Wege GET-Parameter, die per URL übermittelt wurden, zu überprüfen. Wir nehmen als Beispiel die URL
http://www.domain.de/seite.php?id=1&parameter=abc

Solche URLs sind immer von der Gefahr betroffen, dass die Parameter einfach über die Browserzeile bearbeitet werden können. Mit POST-Parametern geht das auch, aber es ist nicht ganz so einfach (ich sehe schon die Kommentare dazu, wie einfach das ist … 😉 )
Jedenfalls werden diese Parameter anschließend meistens in einer SQL-Abfrage verwendet. Zum Beispiel so:

SELECT spalte1,spalte2 
FROM tabelle 
WHERE ID=".$_GET['id']."

Das ist aber sehr gefährlich, da recht einfach eine SQL Injection durchgeführt werden kann. Deshalb muss die GET-Variable gefiltert werden (oder zumindest überprüft auf schädliche Zeichen).

Ein Ansatz der Filterung wäre ungültige Zeichen zu ersetzen – ganz am Anfang des Scripts.

$_GET['id'] = str_replace(array("'","\"",";",...),"",$_GET['id']);

Das ganze kann man auch über einen regulären Ausdruck lösen, das nenne ich aber mal im gleichen Atemzug.

Eleganter ist die Lösung über die Funktion mysql_real_escape_string. Diese benötigt allerdings zwingend eine aufgebaute Datenbankverbindung (würde hier funktionieren, aber es soll ja auch andere DBMS oder dynamische Seiten ohne Datenbank geben…).

Es wäre auch settype() oder ein Typ-Cast möglich, aber diese Prüfungen sind alle PHP-basiert, weshalb ich sie mal in eine Gruppe stecken möchte, um sie gegen folgende Variante zu vergleichen.

Die Idee, die ich nun letztens angewendet habe, basiert nicht auf PHP. Sie ergibt sich aus dem Einsatz von mod-rewrite. Dieses Modul für den Apache (und auch andere Server) ermöglicht das dynamische Umschreiben von URLs mit Hilfe von regulären Ausdrücken. Oben genannte URL könnte damit zum Beispiel so aussehen:
http://www.domain.de/seite-1-abc.html
Das hat vor allem auch für Suchmaschinen positive Auswirkungen, denn solche URLs sind erstens statisch und zweitens meist aussagekräftiger, weil Sie noch den Seitentiel oder ähnliche Schlüsselwörter enthalten. Aber das nur am Rande, hier gehts ja mehr um den Performance- und Sicherheitsaspekt an der Geschichte.

Wir gehen einmal davon aus, dass die zu der oben genannten SQL-Abfrage gehörige Spalte ID vom Typ UNSIGNED INT ist.
Welche mod-rewrite-Regel können wir also nun zum Umschreiben verwenden? Da gibt es mehrere Möglichkeiten, z.B.:
RewriteRule ^seite-(.*)-(.*).html$ seite.php?id=$1&parameter=$2 [L]
Und genau hier liegt der Hase im Pfeffer. Man sollte aus Sicherheitsgründen den Wertebereich in den mod-rewrite-Regeln so eng wie möglich (aber natürlich so weit wie nötig) fassen.
Viel besser ist deshalb folgende Variante:
RewriteRule ^seite-([0-9]+)-(.+).html$ seite.php?id=$1&parameter=$2 [L]
Nun können bei der ID auch wirklich nur noch numerische Werte übergeben werden. Folgende URL würde nun also nicht mehr funktionieren (404-Fehler):
seite-1 OR 1-abc.html

Genau diese URL wäre eine mögliche Angriffs-URL gewesen. Aber es gäbe noch viele weitere…
Wenn der mögliche Wertebereich für Parameter noch enger eingeschränkt ist – besonders bei einer überschaubaren Wertemenge der Größe 2 oder 3, kann man die Regel noch restriktiver fassen. Wir nehmen an, dass die Variable parameter nur die Werte „hilfe“,“seite“ und „datei“ annehmen kann. Dann können wir die Regel so formulieren:
RewriteRule ^seite-([0-9]+)-(hilfe|seite|datei).html$ seite.php?id=$1&parameter=$2 [L]

Die Variante über die .htaccess-Datei ist zudem schneller als die PHP-Variante (egal, wie gefiltert wird). Somit kann die .htaccess 3 Fliegen mit einer Klappe schlagen: suchmaschinenfreundliche URLs, Sicherheitsgewinn, Performancegewinn.

Übrigens habe ich es nicht geschafft Abkürzungen wie \d für Ziffern hier zu benutzen. Ich weiß nicht, ob das an einer Apache-Einstellung liegt oder ob das grundsätzlich nicht möglich ist.

Ich denke ich konnte diese einfache Methode etwas näher bringen, denn ich sehe immer wieder, dass nur einfach (.*) und (.+) in den Regeln benutzt wird. Das sollte man vermeiden, es sei denn der Wertebereich ist wirklich unüberschaubar (Produktnamen bei einem Onlineshop, die in der URL auftauschen sollen).

Jan hat 152 Beiträge geschrieben

22 Kommentare zu “URL-Manipulationen verhindern

  1. Leif sagt:

    Zitat: „Die Variante über die .htaccess-Datei ist zudem schneller als die PHP-Variante“.

    Warum hat Apache eine schnellere RegEx-Engine als PHP?
    Außerdem muss Apache bei jedem Seitenaufruf in dem Verzeichnis die RewriteRule überprüfen. IIRC hatte ich mal gelesen, dass das verlangsamend wirkt.

  2. Hasch sagt:

    Alles schön und gut, dann lassen sich diese Variablen nicht mehr über die Umschreibung manipulieren. Ohne Prüfung seitens PHP kommst du trotz dessen nicht weg, weil ich ebenso die herkömliche Art verwenden kann ala seite.php?var=bla et cetera pp. (Dort gibt es dann keinen Apache, der prüft, also unnötige Ressourcenverschwendung… ;))

  3. Denis sagt:

    Ich fühle mich trotzdem sicherer, wenn ich jede Variable einzeln sichere…

    $p_id = $_GET[‚id‘];

    if(!is_numeric($p_id) || $p_id

    Da kann dann auch ich beruhigt schlafen. 🙂

  4. robo47 sagt:

    @Hasch

    Aber das kann man ja eigentlich recht einfach abfangen oder ?

    Dazu muss man nur überprüfen ob $_SERVER[‚REDIRECT_*‘] Variablen gesetzt sind bzw. deren Werte stimmen.
    wenn nicht wird 404 oder ähnliches angezeigt.

    mfg
    robo47

  5. GhostGambler sagt:

    WordPress suckt.

    btw. ich halte nichts davon die Überprüfung NUR im Apache zu machen.
    Bei einer Klasse lasse ich die interne Typenüberprüfung ja auch nicht weg, weil ich davon ausgehe, dass der Methode nur richtige Werte übergeben werden!

  6. tcomic sagt:

    Das sehe ich genau so wie GhostGambler!
    Every input is evil === every input must be validated.

    Ich fange daher immer am Anfang des Scripts alle POST, GET etc. Variablen ab und parse diese mit einer eigenen Klasse. Dies sieht dann in etwa so aus:
    $input = new Input();
    $_POST = $input->validate($_POST);
    $_GET = $input->validate($_GET);
    etc…
    Intern wird dann aller böse Code entfernt, ev. BBCode in lesbaren HTML Code umgewandelt und gut ist.

  7. loci sagt:

    wieso hat php denn seit 5.2 eine eingebaute inputvalidierung? die ist compiled und sollte dementsprechend auch flott sein. vom anwendungskomfort, der wartbarkeit etc. ganz zu schweigen.

  8. Hasch sagt:

    @robo47 Womit willst du dies abfangen? Im Apache oder mit PHP? Sicherlich kann man auch uebergebene Variablen mit dem Apache pruefen, aber dieser wird dann richtig langsam (bsw. durch mod_security). PHP validiert da wesentlich schneller, zumal PHP Anfragen cachen kann, der Apache aber jede Anfrage wieder neu aufbauen und ausfuehren muss. Zumal kann man mod_security Anweisungen meines Wissens nach nur in der httpd.conf angeben, was einer Produktivumgebung im Wege steht…

  9. robo47 sagt:

    @Hasch

    In php. Ich kann ganz einfach feststellen OB die anfrage durch das mod_rewrite ging oder ob das script direkt aufgerufen wurde, indem ich überprüfe ob $_SERVER[‚REWRITE_URL‘] gesetzt ist oder nicht. Rufe ich direkt index.php?param=böse auf ist die Variable nicht gesetzt und ich gebe nen Fehler aus. Wäre eine Möglichkeit. Ich würde sowas nicht nutzen, aber die Möglichkeit besteht.

  10. GhostGambler sagt:

    „Zumal kann man mod_security Anweisungen meines Wissens nach nur in der httpd.conf angeben, was einer Produktivumgebung im Wege steht…“

    Versteh ich nicht…

    .htaccess auf einem vernünftigen Server ist sowieso eine eher schwache Idee. Eine Konfig über httpd.conf ist schneller, wenn man die .htaccess Unterstützung ganz abschaltet, weil der Apache dann nicht rekursiv nach eben den .htaccess-Dateien in den ganzen Verzeichnissen suchen muss.

  11. Hasch sagt:

    @robo47: Dann wäre dies eine Möglichkeit.
    @GhostGambler: Ja, aber nicht jeder hat einen Server, die Mehrheit der PHP-Anwendungen läuft auf einem Hoster, der nur .htaccess möglich macht.

  12. Leif sagt:

    Das mit dem is_numeric() ist ein recht häufiger Fehler, der auch oft von Programmierer zu Programmierer weitergereicht wird.
    IDs in der URL sind normalerweise schlicht Ganzzahlen. Also sollte man ctype_digit() nehmen.

    Probier doch mal folgendes:

    var_dump(is_numeric(‚13.37‘));
    var_dump(is_numeric(‚+13.37e89‘));
    var_dump(is_numeric(‚0xAB‘));

    Ist alles true.

    Außerdem ist is_numeric() langsamer als ctype_digit(). Performance ahoi.

  13. Leif sagt:

    Ja, die Methode wollte ich noch gegenüberstellen, habe es dann aber gelassen.
    Der Unterschied ist, dass du mit ctype_digit() prüfen kannst, ob da was richtiges in der URL steht. Wenn nicht, kannst du reagieren. Du weißt, dass dein Script fehlerhaft ist oder jemand manipulieren wollte.

    (int) und intval() bringen zwar keine Fehlermeldung, aber dein Script arbeitet mit einer falschen ID weiter und macht andere Sachen als geplant. Manipulationen kommst du so auch nicht auf die Spur.

  14. Robert sagt:

    Oha, das hatte ich so genau noch nicht bertachtet, dafür besten Dank. Nun ist mir mal wieder bewusst geworden, dass bequem nicht immer wirklich gut ist, selbst wenn alles so funktioniert wie gewollt. Danke.

  15. Heiko sagt:

    @admin:
    Der Blog ist wirklich gut, bin gerade durch Google hierauf gestoßen und werde mir mal etwas Zeit nehmen deine Tipps durch zu arbeiten, aber an deinem Wissen über SPAM, Marketing und Suchmaschinen musst du wohl noch etwas feilen 😉
    Denn der „Jürgen aus der Pfalz“ wollte dir kein Kompliment machen und will auch nicht wirklich mehr wissen. Er wollte nur Werbung für seine Webseite machen, auch wenn ihm das, dank rel=“nofollow“ suchmaschinentechnisch nicht viel bringt 😉

  16. admin sagt:

    Hmm stimmt, hab ich auch grad gesehen, wo Du es sagst. Hab mich an deutsche Spamkommentare noch nicht so gewöhnt 😉 Wenn ein englischer Kommentar sagt, dass meine Seite toll ist, gehen die Alarmglocken eher an 😉

  17. Martin sagt:

    „(int) und intval() bringen zwar keine Fehlermeldung, aber dein Script arbeitet mit einer falschen ID weiter und macht andere Sachen als geplant. Manipulationen kommst du so auch nicht auf die Spur.“

    Welches ist denn die „richtige“ id, wenn es einen Injektionversuch gab? Warum sollte es mich interessieren, ob der „falsche“, welcher auch immer „falsch“ sein sollte, z.B. Artikel angezeigt wird? Bei gewollten IDs mir führender) Null ist int und intval aber Essig.

    @ Autor, ich wüßte auch nicht, warum ich mysql_real_escape_string verwenden wollen sollte, wenn ich mit was anderem als mysql oder ganz ohne Datenbank arbeite, in letzterem Fall, können mir Injektionversuche häufig ganz egal sein.

  18. Melanie sagt:

    Um SQL-Injection vorzubeugen verwende ich in meinen eigenen Projekten (= kleine selbst geschriebene Scripts) nur noch Prepared Statements. Ich bin meist nicht auf Performance angewiesen und kann es mir daher erlauben Prepared Statements einzusetzen. Der User Input wird natürlich trotzdem noch validiert schon alleine um unbeabsichtigte Fehleingaben abzufangen.

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>