SQL-Injection-Prävention

  1. Keine Panik.
  2. Was ist eine SQL-Injection?
  3. Was sind die Formatierungsregeln?
  4. Warum ist die manuelle Formatierung schlecht?
  5. Vorbereitete Aussagen.
  6. Einen Schritt weiter.
  7. Ein kleiner Trick.
  8. Ausnahme, die die Regel bestätigt.
  9. Dynamisch erstellte Abfragen.
  10. Fazit.
  11. Anhang 1. Wortschatz.
  12. Anhang 2. Schutz vor Injektionen vom Typ [xxx].
  13. Anhang 3. Falsche Maßnahmen und schlechte Praktiken.
  14. Anhang 4. ORMs und Query Builder.
  15. TL; DR

Haftungsausschluss: Mein Englisch ist alles andere als perfekt und dieser Text wurde geschrieben, als es noch schlimmer war. Wenn Sie eine so schlechte Grammatik nicht ausstehen und es sich leisten können, ein bisschen Korrektur zu lesen, finden Sie hier die Quelle zu Github , auf die alle Anfragen dankbar eingehen.

In diesem Artikel werde ich versuchen, die Art der SQL-Injection zu erklären. Zeigen Sie, wie Sie Ihre Abfragen zu 100% sicher machen können. und zerstreuen Sie zahlreiche Wahnvorstellungen, Aberglauben und schlechte Praktiken im Zusammenhang mit dem Thema SQL Injection Prevention.

Keine Panik.

Ehrlich gesagt gibt es keinen einzigen Grund, in Panik zu geraten oder sich Sorgen zu machen. Alles, was Sie brauchen, ist, ein paar alte Aberglauben loszuwerden und ein paar einfache Regeln zu lernen, um alle Ihre Fragen sicher zu stellen. Genau genommen müssen Sie nicht einmal vor SQL-Injektionen schützen! Das heißt, es müssen keine speziellen Maßnahmen ergriffen werden, die ausschließlich zum Schutz vor SQL-Injection gedacht sind. Sie müssen Ihre Abfrage nur richtig formatieren . So einfach ist das. Glaubst du mir nicht? Bitte lesen Sie weiter.

Was ist eine SQL-Injection?

SQL Injection ist ein Exploit einer falsch formatierten SQL-Abfrage.

Die Wurzel der SQL-Injection ist die Vermischung von Code und Daten.
Tatsächlich ist eine SQL-Abfrage ein Programm. Ein völlig legitimes Programm - genau wie unsere bekannten PHP-Skripte. Es kommt also vor, dass wir dieses Programm dynamisch erstellen und diesem Programm spontan Daten hinzufügen. Natürlich können diese Daten den Programmcode stören und ihn sogar ändern - und eine solche Änderung wäre die eigentliche SQL-Injection.

Dies kann jedoch nur passieren, wenn Abfrageteile nicht ordnungsgemäß formatiert werden. Schauen wir uns ein kanonisches Beispiel an :

 $name  "Bobby';DROP TABLE users; -- ";
$query "SELECT * FROM users WHERE name='$name'";

die kompiliert in die bösartige Reihenfolge

 SELECT FROM users WHERE name='Bobby';DROP TABLE users; -- '

Nennen Sie es eine Spritze? Falsch. Es ist ein falsch formatiertes String-Literal.
Was, einmal richtig formatiert, niemandem schadet:

 SELECT FROM users WHERE name='Bobby\';DROP TABLE users; -- '

Nehmen wir ein weiteres kanonisches Beispiel:

 $id    "1; DROP TABLE users;"
$id    mysqli_real_escape_string($link$id);
$query "SELECT * FROM users where id = $id";

mit nicht weniger schädlichem Ergebnis:

 SELECT FROM users WHERE id =1;DROP TABLE users; -- '

Nennen Sie es noch einmal eine Spritze? Schon wieder falsch. Es ist ein falsch formatiertes numerisches Literal. Sei es richtig formatiert, ein ehrlicher

 SELECT FROM users where id 1

Aussage wäre positiv harmlos.

Aber der Punkt ist, wir müssen die Abfragen trotzdem formatieren - egal ob es eine Gefahr gibt oder nicht. Angenommen, es gab keine Bobby Tables, sondern ein ehrliches Mädchen namens Sarah O'Hara- das würde niemals in eine Klasse kommen, wenn wir unsere Abfrage nicht formatieren würden, einfach weil die Anweisung

 INSERT INTO users SET name='Sarah O'Hara'

verursacht einen normalen Syntaxfehler.

Also müssen wir nur um es zu formatieren. Nicht für Bobby, sondern für Sarah. Das ist der Punkt.

Während SQL Injection nur eine Folge einer falsch formatierten Abfrage ist.

Darüber hinaus geht die ganze Gefahr von der fraglichen Aussage aus: Unzählige PHP-Benutzer glauben immer noch, dass der mysqli_real_escape_string()einzige Zweck der berüchtigten Funktion darin besteht, "SQL vor Injektionen zu schützen" (durch das Entkommen einiger fiktiver "gefährlicher Zeichen"). Wenn sie nur den wahren Zweck dieser ehrlichen Funktion kennen würden, gäbe es keine Injektionen auf der Welt! Wenn sie ihre Abfragen nur richtig formatieren würden , anstatt sie zu "schützen", hätten sie einen wirklichen Schutz.

Um unsere Abfragen unverwundbar zu machen, müssen wir sie ordnungsgemäß formatieren und eine solche Formatierung obligatorisch machen . Nicht nur als gelegentliche zufällige Behandlung, wie es häufig vorkommt, sondern als strenge, unantastbare Regel. Und Ihre Abfragen sind nur als Nebeneffekt absolut sicher .

Was sind die Formatierungsregeln?

Die Wahrheit ist, Formatierungsregeln sind nicht so einfach und können nicht in einem einzigen Imperativ ausgedrückt werden. Dies ist der interessanteste Teil, da SQL-Abfragen nach Ansicht eines durchschnittlichen PHP-Benutzers homogen und solide sind wie ein PHP-String-Literal, das sie repräsentiert. Sie behandeln SQL als solides Medium , für das nur allumfassendes "SQL-Escape" erforderlich ist. Tatsächlich fragt SQL ein Programm ab , genau wie ein PHP-Skript. Ein Programm mit einer eigenen Syntax für seine eigenen Teile, und jeder Teil benötigt eine eigene Formatierung, die für die anderen nicht anwendbar ist!

Für MySQL wäre es:

  1. Streicher
    • müssen über eine native vorbereitete Anweisung
      oder hinzugefügt werden
    • müssen in Anführungszeichen gesetzt werden
    • Sonderzeichen (ehrlich gesagt - die sehr einschränkenden Anführungszeichen) müssen maskiert werden
    • Es muss eine korrekte Client-Codierung eingestellt werden
      oder
    • könnte hex-codiert sein
  2. Zahlen
    • müssen über eine native vorbereitete Anweisung
      oder hinzugefügt werden
    • sollte herausgefiltert werden, um sicherzustellen, dass nur numerische Zeichen, ein Dezimaltrennzeichen und ein Vorzeichen vorhanden sind
  3. Bezeichner
    • müssen in Backticks eingeschlossen werden
    • Sonderzeichen (ehrlich gesagt - die sehr einschränkenden Backticks) müssen ausgeblendet werden
  4. Operatoren und Schlüsselwörter.
    • Es gibt keine speziellen Formatierungsregeln für die Schlüsselwörter und Operatoren, außer dass es sich um legitime SQL-Operatoren und Schlüsselwörter handeln muss. Sie müssen also auf die Whitelist gesetzt werden .

Wie Sie sehen, gibt es nicht nur eine einzige Anweisung, sondern vier ganze Regelsätze . Man kann sich also nicht an einen magischen Gesang wie "Entkomme deinen Eingaben" oder "Verwende vorbereitete Anweisungen" halten.

"In Ordnung" - würden Sie sagen - "Ich befolge bereits all diese Regeln. Worum geht es in der ganzen Aufregung?" Die ganze Aufregung dreht sich um die manuelle Formatierung. Tatsächlich sollten Sie diese Regeln niemals manuell im Anwendungscode anwenden, sondern müssen über einen Mechanismus verfügen, der diese Formatierung für Sie übernimmt. Warum?

Warum ist die manuelle Formatierung schlecht?

Weil es manuell ist. Und manuell bedeutet fehleranfällig. Es hängt vom Können, Temperament, der Stimmung, der Anzahl der Biere der letzten Nacht usw. des Programmierers ab. In der Tat ist die manuelle Formatierung der einzige Grund für die meisten Injektionsfälle weltweit. Warum?

  1. Die manuelle Formatierung kann unvollständig sein. Nehmen wir den Fall der Bobby Tables. Es ist ein perfektes Beispiel für eine unvollständige Formatierung: Eine Zeichenfolge, die wir der Abfrage hinzugefügt haben, wurde nur in Anführungszeichen gesetzt, aber nicht maskiert! Während, wie wir gerade aus dem Obigen gelernt haben, Anführungszeichen und Escapezeichen immer zusammen gehören sollten (zusammen mit dem Einstellen der richtigen Codierung für die Escapezeichenfunktion). In einer gewöhnlichen PHP-Anwendung, die SQL-Zeichenfolgen separat formatiert (teilweise in der Abfrage und teilweise an einer anderen Stelle), ist es jedoch sehr wahrscheinlich, dass ein Teil der Formatierung einfach übersehen wird.

  2. Die manuelle Formatierung kann auf das falsche Literal angewendet werden . Keine große Sache, solange wir die vollständige Formatierung verwenden (da dies zu einem sofortigen Fehler führt, der in der Entwicklungsphase behoben werden kann), aber in Kombination mit einer unvollständigen Formatierung ist dies eine echte Katastrophe. Auf der großartigen Website von Stack Overflow gibt es Hunderte von Antworten, die darauf hindeuten , Bezeichner wie Zeichenfolgen zu umgehen. Das wäre völlig nutzlos und würde eine SQL-Injection verursachen.

  3. Die manuelle Formatierung ist im Wesentlichen eine unverbindliche Maßnahme . Erstens gibt es einen offensichtlichen Mangel an Aufmerksamkeit, bei dem die richtige Formatierung einfach vergessen werden kann. Aber es gibt einen wirklich seltsamen Fall: Viele PHP-Benutzer lehnen absichtlich jede Formatierung ab, weil sie die Daten bis heute in "Bereinigen" und "Unreinigen", "Benutzereingabe" und "Nicht-Benutzereingabe" trennen. usw. Denken, dass "sichere" Daten keine Formatierung benötigen. Welches ist ein einfacher Unsinn - erinnern Sie sich an Sarah O'Hara. Aus der Sicht der Formatierung ist es das Ziel , das zählt. Ein Entwickler muss auf die Art des SQL-Literal achten, nicht die Datenquelle. Geht es um eine Zeichenfolge für die Abfrage? Es muss dann als String formatiert werden. Egal, ob es sich um eine Benutzereingabe handelt oder nur auf mysteriöse Weise inmitten der Codeausführung auftaucht.

  4. Die manuelle Formatierung kann erheblich von der eigentlichen Abfrageausführung entfernt sein.

Das am meisten unterschätzte und übersehene Thema. Das Wesentlichste von allen, da es alle anderen Regeln allein verderben kann, wenn es nicht befolgt wird.

Fast jeder PHP-Benutzer ist versucht, die gesamte "Bereinigung" an einem einzigen Ort durchzuführen, weit entfernt von der eigentlichen Abfrageausführung, und ein derartiger falscher Ansatz führt allein zu unzähligen Fehlern:

  • Erstens kann man, wenn man keine Abfrage zur Hand hat, nicht sagen, für welche Art von SQL-Literal ein bestimmtes Stück steht - und daher haben wir beide Formatierungsregeln (1) und (2) gleichzeitig verletzt.
  • Wenn mehr als ein Ort für die Bereinigung vorhanden ist (dies kann entweder eine zentralisierte Einrichtung oder eine direkte Formatierung sein), fordern wir eine Katastrophe, da ein Entwickler denkt, dass dies von einem anderen getan wurde oder bereits an einem anderen Ort durchgeführt wurde usw.
  • Wenn wir mehr als einen Ort für die Bereinigung haben, besteht die Gefahr, dass Daten doppelt bereinigt werden (z. B. ein Entwickler formatiert sie am Einstiegspunkt und ein anderer - vor der Ausführung von Abfragen), was nicht gefährlich ist, aber die Website äußerst unprofessionell aussieht
  • Eine vorzeitige Formatierung beeinträchtigt die Quellvariable und macht sie für andere Zwecke unbrauchbar.
  1. Schließlich nimmt die manuelle Formatierung immer zusätzlichen Platz im Code ein, wodurch er verwickelt und aufgebläht wird. Okay, jetzt vertraust du mir, dass die manuelle Formatierung schlecht ist. Was müssen wir stattdessen verwenden?

Vorbereitete Aussagen.

Hier muss ich eine Weile innehalten, um den wichtigen Unterschied zwischen der Implementierung von nativen vorbereiteten Anweisungen, die vom Haupt-DBMS unterstützt werden, und der allgemeinen Idee, einen Platzhalter zur Darstellung der tatsächlichen Daten in der Abfrage zu verwenden, hervorzuheben . Und um den wirklichen Nutzen einer vorbereiteten Aussage hervorzuheben .

Die Idee einer systemeigenen vorbereiteten Anweisung ist intelligent und einfach: Die Abfrage und die Daten werden getrennt voneinander an den Server gesendet, sodass keine Möglichkeit besteht, dass sie sich gegenseitig stören. Das macht SQL-Injection komplett unmöglich. Gleichzeitig hat die native Implementierung ihre Grenzen, da sie nur zwei Arten von Literalen (Strings und Zahlen) unterstützt, die für den tatsächlichen Gebrauch unzureichend und unsicher sind.

Es gibt auch einige falsche Aussagen über native vorbereitete Aussagen:

  • Sie sind "schneller". Nicht in PHP , mit dem Sie eine vorbereitete Anweisung zwischen verschiedenen Aufrufen einfach nicht wiederverwenden können. Während wiederholte Abfragen innerhalb derselben Instanz zu selten auftraten, um darüber zu sprechen.
  • Sie sind "sicherer". Dies ist teilweise richtig, aber nicht, weil sie ursprünglich sind , sondern weil sie vorbereitete Anweisungen sind - im Gegensatz zur manuellen Formatierung.

Und hier kamen wir zum Hauptpunkt des gesamten Artikels: Die allgemeine Idee, eine SQL-Abfrage aus konstanten Teilen und Platzhaltern zu erstellen, die durch tatsächliche Daten ersetzt wird, die automatisch formatiert werden, ist in der Tat ein Heiliger Gral, den wir gesucht haben .

Der wichtigste und wichtigste Vorteil von vorbereiteten Anweisungen ist die Beseitigung aller Gefahren der manuellen Formatierung:

  • Eine vorbereitete Anweisung übernimmt die vollständige Formatierung - alles ohne Eingreifen des Programmierers! Einfach feuern und vergessen.
  • Eine vorbereitete Anweisung übernimmt die angemessene Formatierung (solange wir unsere Daten mit dem richtigen Typ binden).
  • Eine vorbereitete Anweisung macht die Formatierung unantastbar!
  • Eine vorbereitete Anweisung führt die Formatierung an der einzig richtigen Stelle durch - unmittelbar vor der Ausführung der Abfrage.

Aus diesem Grund wird die manuelle Formatierung heutzutage so sehr verachtet und vorbereitete Aussagen werden so honoriert.

Die Verwendung von vorbereiteten Anweisungen bietet zwei zusätzliche, jedoch nicht wesentliche Vorteile:

  • Eine vorbereitete Anweisung verderbt nicht die Quelldaten, die sicher an einem anderen Ort verwendet werden können: Sie werden im Browser angezeigt, in einem Cookie gespeichert usw.
  • Ein Programmierer, der faul genug ist, kann seinen Code mithilfe von vorbereiteten Anweisungen drastisch verkürzen (aber auch das Gegenteil ist der Fall - ein sorgfältiger Benutzer kann einen Code für die einfache Einfügung der Größe eines durchschnittlichen Romans schreiben).

Daher ist die "Ursprünglichkeit" einer vorbereiteten Anweisung nicht so wichtig, wie PDO nachweist. PDO kann lediglich eine vorbereitete Anweisung emulieren , die reguläre Abfrage sofort an den Server senden und Platzhalter durch die tatsächlichen Daten ersetzen, wenn die PDO::ATTR_EMULATE_PREPARES Konfigurationsvariable festgelegt ist zu TRUE. In diesem Fall werden die Daten jedoch ordnungsgemäß formatiert - und daher ist dieser Ansatz ebenso sicher!

Außerdem können wir auch mit der alten MySQL-Erweiterung vorbereitete Anweisungen verwenden! Hier ist eine kleine Funktion, die mit dieser alten guten Erweiterung eine absolut zuverlässige Sicherheit bieten kann:

 function paraQuery()
{
    
$args  func_get_args();
    
$query array_shift($args);
    
$query str_replace("%s","'%s'",$query); 

    foreach (
$args as $key => $val)
    {
        
$args[$key] = mysql_real_escape_string($val);
    }

    
$query  vsprintf($query$args);
    
$result mysql_query($query);
    if (!
$result)
    {
        throw new 
Exception(mysql_error()." [$query]");
    }
    return 
$result;
}

$query  "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result paraQuery($query$a"%$b%"$limit);

Schauen Sie - alles ist parametriert und sicher, zumindest so weit das PDO es bieten kann.

Also die umfassende Regel für den Schutz:

Jedes dynamische Element sollte über einen Platzhalter abgefragt werden

Hier muss ich noch einmal innehalten, um eine sehr wichtige Aussage zu treffen: Wir müssen ein konstantes Abfrageelement von einem dynamischen unterscheiden . Unser Hauptanliegen müssen natürlich dynamische Teile sein, nur weil sie sehr dynamisch sind. Während der konstante Wert nicht durch das Design beeinträchtigt werden kann und das Formatierungsproblem in der Entwicklungsphase behoben werden kann, ist das dynamische Abfrageelement eine eigenständige Angelegenheit. Aufgrund seiner variablen Natur können wir niemals feststellen, ob es einen gültigen Wert enthält oder nicht. Aus diesem Grund ist es so wichtig, Platzhalter für alle dynamischen Abfrageteile zu verwenden.

"In Ordnung" - sagt Ihr - "Ich verwende bereits vorbereitete Aussagen. Und was nun?" Seien Sie ehrlich - Sie sind nicht.  

Einen Schritt weiter.

Tatsächlich sind unsere Abfragen manchmal nicht so primitiv wie die Suche nach Primärschlüsseln. Manchmal müssen wir sie noch dynamischer machen, indem wir Bezeichner oder andere komplexe Strukturen wie Arrays hinzufügen. Welche regelmäßigen Fahrer bieten an, um dieses Problem zu lösen?

Nichts.

Für den Bezeichner wäre es nichts anderes ... als eine alte gute manuelle Formatierung:

 $field "`".str_replace("`","``",$field)."`";
$sql   "SELECT * FROM t ORDER BY $field";
$data  $db->query($sql)->fetchAll();

Für die Arrays wird ein ganzes Programm geschrieben, um eine Abfrage im laufenden Betrieb zu erstellen:

 $ids = array(1,2,3);
$in  str_repeat('?,'count($arr) - 1) . '?';
$sql "SELECT * FROM table WHERE column IN ($in) AND category=?";
$stm $db->prepare($sql);
$ids[] = $category//adding another member to array
$stm->execute($ids);
$data $stm->fetchAll();

Und dennoch haben wir eine Variable in der Abfragezeichenfolge interpoliert, die mich im Hintergrund erschaudern lässt, obwohl ich sicher bin, dass dieser Code sicher ist. Und es sollte dich auch zittern lassen!

In all diesen Fällen sind wir gezwungen, in die Steinzeit der manuellen Formatierung zurückzukehren!

Aber solange wir erfahren haben, dass die manuelle Formatierung schlecht und vorbereitete Anweisungen gut sind, gibt es nur eine mögliche Lösung: Wir müssen Platzhalter auch für diese Typen implementieren!

Stellen Sie sich vor, wir haben einen Platzhalter für die oben genannten Fälle. Diese beiden Ausschnitte werden mit vorbereiteten Anweisungen so einfach und sicher wie jeder andere Code:

 $sql   "SELECT * FROM t ORDER BY ?";
$data  $db->query($sql$field)->fetchAll();

$stm $db->prepare("SELECT * FROM table WHERE column IN (?) AND category=?");
$stm->execute([$ids$category]);
$data $stm->fetchAll();

Hier können wir also nur eine Schlussfolgerung ziehen: Regelmäßige Treiber müssen erweitert werden, um einen größeren Bereich von zu bindenden Datentypen zu unterstützen. Nachdem wir uns eine Idee über neue Typen ausgedacht haben, können wir uns überlegen, was diese Typen sein können:

  • Identifier-Platzhalter (Single Identifier)
  • Bezeichnerliste (durch Kommas getrennte Bezeichner)
  • Ganzzahlliste (durch Kommas getrennte Ganzzahlen)
  • Zeichenfolgenliste (durch Kommas getrennte Zeichenfolgen)
  • Der spezielle SET-Typ besteht aus durch Kommas getrennten idientifier=string valuePaaren
  • Sie nennen es

Sehr viel zusätzlich zu bestehenden Typen.  

Ein kleiner Trick.

So weit, ist es gut. Aber hier stehen wir vor einem anderen Problem. Selbst bei regulären Typen müssen wir manchmal den Datentyp explizit festlegen, damit der Treiber versteht, wie dieser bestimmte Wert formatiert wird. Ein klassisches Beispiel für PDO:

 $stm $db->prepare('SELECT * FROM table LIMIT ?, ?');
$stm->execute([1,2]);

Funktioniert nicht im Emulationsmodus, da PDO Daten als Zeichenfolgen formatiert, die in diesem Abfrageteil nicht zulässig sind. Es ist jedoch kein großes Problem, da die Standardformatierung von Zeichenfolgen die meiste Zeit in Ordnung ist:

 $stm $db->prepare('SELECT * FROM t WHERE id=? AND email=?');
$stm->execute([$id,$email]);

Aber mit unseren neuen komplexen Typen gibt es keine Möglichkeit mehr, diesen Ansatz anzuwenden. Ein Bezeichner kann beispielsweise niemals als Zeichenfolge gebunden werden. Daher müssen wir den Platzhaltertyp immer explizit festlegen. Und es macht ein Problem: Obwohl sowohl PDO als auch Mysqli ihre eigenen Lösungen anbieten, würde ich keine von ihnen als brauchbar bezeichnen.

  • Mit PDOs Tonnen von Zeilen ist bindValueunser Code mysql_real_escape_stringin Bezug auf die Codegröße und die Anzahl der Wiederholungen nicht besser als der alte Ansatz.
  • mysqlis bind_param () ist ein Albtraum, wenn Sie eine variable Anzahl von Werten binden müssen.
  • Das Schlimmste ist, dass all diese manuellen Bindungen den Anwendungscode aufblähen lassen, da wir diese Bindungen nicht in einige Interna einbetten können.

Wir brauchen also wieder eine bessere Lösung. Und hier ist es:

Platzhalter mit Typ kennzeichnen!

Keine ganz frische Idee, muss ich zugeben. Die bekannte printf()Funktion verwendet %sgenau dieses Prinzip seit der Unix-Epoche: Wird als Zeichenfolge, %dals Ziffer usw. formatiert . Wir müssen uns also nur diese geniale Idee für unsere Bedürfnisse ausleihen.

Um alle Probleme zu lösen, müssen wir einen regulären Treiber mit einem einfachen Parser erweitern, der eine Abfrage mit typenbezogenen Platzhaltern analysiert, Typinformationen extrahiert und zum Formatieren eines Werts verwendet.

Ich bin auch nicht der erste, der diesen Ansatz mit DBAL anwendet. Es gibt Dibi- , DBSimple- oder NikiC-PDO-Wrapper und einige andere Beispiele. Aber es scheint mir, dass ihre Autoren die Bedeutung von typgetönten Platzhaltern unterschätzt haben, indem sie sie als eine Art Syntaxzucker, nicht als wesentliches und Eckpfeiler, der es sein muss, aufgefasst haben.

Deshalb habe ich mich bemüht, die Idee selbst umzusetzen, um sie hervorzuheben, und sie SafeMysql genannt , weil typenbezogene Platzhalter sie in der Tat sicherer machen als den normalen Ansatz. Es zielt auch auf eine bessere Benutzerfreundlichkeit ab, um den Code sauberer und kürzer zu machen.

Obwohl noch einige Verbesserungen für die Implementierung ausstehen (z. B. Unterstützung für native Platzhalter, benannte Platzhalter (wie in PDO), nicht typisierter Standardplatzhalter, der als Zeichenfolge behandelt wird, und einige andere zu unterstützende Datentypen), ist dies bereits eine vollständige Lösung kann in jedem Produktionscode verwendet werden.  

Ausnahme, die die Regel bestätigt.

Leider ist eine vorbereitete Erklärung auch keine Wunderwaffe und kann allein keinen 100% igen Schutz bieten. Es gibt zwei Fälle, in denen dies entweder nicht ausreicht oder überhaupt nicht zutrifft:

  1. Denken Sie an die letzte Art von Abfrage-Literalen aus der Liste der Formatierungsregeln in Abschnitt 3: SQL-Schlüsselwörter. Sie können nicht formatiert werden.
  2. Ein etwas kniffliger Fall, wenn wir unsere Liste der Bezeichner dynamisch auf der Grundlage von Benutzereingaben erstellen und es Felder geben kann, die ein Benutzer nicht darf. In der Regel wird eine Einfügeabfrage dynamisch basierend auf den Schlüsseln und Werten des $_POSTArrays erstellt. Obwohl wir beide richtig formatieren können, gibt es immer noch eine Möglichkeit für ein Feld wie "admin" oder "permissions", die nur vom Site-Administrator festgelegt werden können. Und diese Felder sollten niemals für Benutzereingaben zugelassen werden.

Und um diese beiden Fälle zu lösen, müssen wir einen anderen Ansatz implementieren, der als Whitelisting bezeichnet wird . In diesem Fall muss jeder dynamische Parameter bereits in Ihrem Skript vorab geschrieben sein, und alle möglichen Werte müssen aus dieser Menge ausgewählt werden.
So führen Sie beispielsweise eine dynamische Bestellung durch:

 $orders  = array("name","price","qty"); //field names
$key     array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby $orders[$key]; //if not, first one will be set automatically. smart enough :)
$query   "SELECT * FROM `table` ORDER BY $orderby"//value is safe

Für Schlüsselwörter gelten die gleichen Regeln, aber natürlich ist keine Formatierung verfügbar - daher ist nur Whitelisting möglich und sollte verwendet werden:

 $dir $_GET['dir'] == 'DESC' 'DESC' 'ASC'
$sql "SELECT * FROM t ORDER BY field $dir"//value is safe

Beachten Sie, dass die oben erwähnte SafeMysql-Bibliothek zwei Funktionen für das Whitelisting unterstützt, eine für key => value-Arrays und eine für einzelne Schlüsselwörter, aber jetzt denke ich daran, diese Funktionen weniger zu empfehlen, sondern strenger zu verwenden.  

Dynamisch erstellte Abfragen.

Da dieser Text einen vollständigen Leitfaden für den Injektionsschutz darstellt, kann ich die Erstellung komplexer Abfragen nicht vermeiden. Ein üblicher Fall für die Suche nach mehreren Kriterien. Oder in jedem anderen Fall, in dem beliebige Abfrageteile hinzugefügt oder aus der Abfrage entfernt werden müssen. In diesem Fall gibt es keine Möglichkeit, Platzhalter zu verwenden. Daher benötigen wir einen anderen Mechanismus.

Erstens ist ein Query Builder hier ein König. Die genauen Case-Query-Builder wurden erfunden

 $query 

$users DB::table('users')->select('*');
if (
$fname input::get('first_name'))
{
    
$query->where('first_name = ?'$fname);
}
if (
$lname input::get('last_name'))
{
    
$query->where('last_name = ?'$lname);
}
// and so on
$results $query->get();

Aber manchmal haben wir eine Abfrage, die so komplex ist, dass es zu schmerzhaft ist, sie mit Hilfe von Abfrage-Buildern zu schreiben. In beiden Fällen müssen wir uns jedoch an die einzige Regel erinnern: Alle dynamischen Abfrageteile werden über Platzhalter abgefragt. Und für eine rohe Abfrage können wir einen sehr intelligenten Trick verwenden :

 SELECT 

FROM people 
WHERE 
( first_name = :first_name or :first_name is null)
AND (
last_name = :last_name or :last_name is null)
AND (
age = :age or :age is null)
AND (
sex = :sex or :sex is null)

Auf diese Weise geschrieben, müssen wir nur unsere Variablen, die entweder einen Wert oder NULL enthalten, an diese Platzhalter binden. Für die NULL-Werte würde die Smart-Engine einfach ihre Bedingungen verwerfen und nur diejenigen mit Werten zurücklassen!

In jedem Fall muss berücksichtigt werden, dass die resultierende Abfrage immer nur aus zwei Quellen erstellt werden sollte - entweder einem konstanten Teil oder einem Platzhalter.  

Fazit.

Kurz gesagt, wir können zwei einfache Regeln formulieren:

Auch wenn eine SQL-Abfrage dynamisch erstellt wurde, muss sie nur aus zwei möglichen Arten von Daten bestehen:

  • Konstante Teile im Skript fest codiert
  • Platzhalter für jeden dynamischen Wert

Bei Einhaltung dieser Regeln wird ein 100% iger Schutz garantiert.  

Anhang 1. Wortschatz.

Fast jeder, der Lust hat, über das Thema zu sprechen, verwendet die breite Palette von Wörtern, ohne sich darum zu kümmern, ihre Bedeutung zu verstehen oder - schlimmer noch - überhaupt eine eigene Vorstellung von der Bedeutung zu haben.

Sicherlich ist der berüchtigtste Begriff "Flucht". Jeder versteht es als "Daten bereinigen", "Daten sicher machen", "gefährlichen Zeichen entkommen". Alle diese Begriffe sind im Wesentlichen vage und mehrdeutig. Gleichzeitig ist nach Ansicht eines durchschnittlichen PHP-Benutzers das "Entkommen" stark mit dem alten verbunden mysql_real_escape_thing, so dass all diese Dinge miteinander vermischt werden, was zu dem ältesten und gefährlichsten PHP-Aberglauben führt - " *_escape_stringVerhindert einen Angriff durch Injection", während wir schon gelernt ist es weit davon entfernt.

Stattdessen sollte der einzig richtige Begriff "Formatierung" sein. Während "escaping" sollte nur einen bestimmten Teil der Formatierung von Zeichenfolgen angeben.

Das gleiche gilt für jedes andere Wort aus der Liste der "Desinfektion", "Filterung", "vorbereiteten Anweisungen" und dergleichen . Überlegen Sie sich die Bedeutung (oder besser gesagt, ob sie überhaupt eine bestimmte Bedeutung hat), bevor Sie ein Wort verwenden.

Sogar der Begriff "SQL Injection" selbst ist äußerst zweideutig. Es scheint, dass die meisten Benutzer die Injektion wörtlich als Hinzufügen einer zweiten Abfrage betrachten, wie im Beispiel von Bobby Tables gezeigt. Und versuchen so, durch das Verbieten der Ausführung mehrerer Abfragen zu schützen (was natürlich unnütz wäre). Eine ähnliche Täuschung wird von vielen anderen Personen geteilt, die der Ansicht sind, dass nur DML-Abfragen als injektionsanfällig angesehen werden können (und sich daher überhaupt nicht für Injektionen in SELECT-Abfragen interessieren).

Wenn Sie also Empfehlungen aus verschiedenen Quellen lesen, denken Sie immer daran, dass Sie die Terminologie verstehen und darüber hinaus verstehen müssen, was der Autor gemeint hat (und ob es überhaupt eine bestimmte Bedeutung gibt).  

Anhang 2. Schutz vor Injektionen vom Typ [xxx].

Möglicherweise sind verschiedene Arten von Injektionen zu hören - "blind", "zeitverzögert", "zweiter Ordnung" und Tausende anderer. Man muss verstehen, dass all dies keine unterschiedlichen Methoden sind, um eine Injektion durchzuführen , sondern nur unterschiedliche Methoden, um sie auszunutzen . Zwar gibt es nur eine Möglichkeit, eine Injektion durchzuführen - die Integrität der Abfrage zu brechen. Wenn Sie also die Integrität von Abfragen gewährleisten können, sind Sie gleichzeitig vor Tausenden verschiedener "Arten" von Injektionen geschützt. Und um die Integrität der Abfrage zu erhalten, müssen Sie nur die Abfrage-Literale richtig formatieren.  

Anhang 3. Falsche Maßnahmen und schlechte Praktiken.

  1. Benutzereingabe wird ignoriert. Das ist der König. Eine schwerwiegende Täuschung, die immer noch von vielen PHP-Benutzern (und sogar von OWASP, wie Sie sehen können) geteilt wird. Besteht aus zwei Teilen: "Escape" und "Benutzereingabe":

    • Escapezeichen: Wie oben erwähnt, erledigt es nur einen Teil der Arbeit für nur einen Typ von SQL-Literal. Und wenn es alleine oder nicht am richtigen Ort eingesetzt wird, ist dies ein sicherer Grund für eine Katastrophe.
    • Benutzereingabe: Im Kontext des Injektionsschutzes sollten solche Wörter nicht vorkommen. Jede Variable ist potentiell gefährlich - egal aus welcher Quelle! Mit anderen Worten: Jede Variable muss korrekt formatiert sein, damit sie abgefragt werden kann - unabhängig davon, aus welcher Quelle sie stammt. Auf das Ziel kommt es an. In dem Moment, in dem ein Entwickler beginnt, die Schafe von der Ziege zu trennen, unternimmt er seinen ersten Schritt zur Katastrophe.
    • Darüber hinaus deutet selbst der Wortlaut darauf hin, dass die Masse am Einstiegspunkt entweicht und genau dem magic quotesMerkmal ähnelt, das bereits verachtet, veraltet und aus der Sprache entfernt wurde.
  2. magische Zitate - die materielle Verkörperung des oben genannten Prinzips. Gott sei Dank, es ist bereits aus der Sprache entfernt.

  3. Datenvalidierung . Man muss verstehen, dass die Validierung von Eingabedaten (im Sinne von Benutzereingaben) absolut nichts mit SQL zu tun hat. Ja wirklich. Keine Validierungsregel kann gegen SQL-Injection helfen, wenn ein Freiformtext zulässig ist. Wir müssen unsere SQL trotzdem formatieren - erinnern Sie sich an Sarah O'Hara, die einen Namen trägt, der vom Standpunkt der Benutzereingabe aus absolut gültig ist. Denken Sie auch daran, dass sich die Validierungsregeln möglicherweise ändern .

  4. htmlspecialchars (und auch filter_var(), strip_tags()und dergleichen).
    Leute. Es ist HTML- Sonderzeichen-Kodierung, wenn Sie es noch nicht bemerkt haben. Es hat absolut nichts mit SQL zu tun . Es hilft nichts in der Sache und sollte niemals im Kontext des SQL-Injektionsschutzes verwendet werden. Es ist für SQL absolut nicht anwendbar und kann Ihre Abfrage auch dann nicht schützen, wenn es als Zeichenketten-Escape-Funktion verwendet wird. Lassen Sie es für andere Teile Ihrer Anwendung.
    Bitte haben Sie auch Verständnis dafür, dass die SQL-Formatierung niemals die Daten verändern sollte. Dann legen Sie Ihren Schmuck in einen Safe und möchten, dass er wieder intakt ist, einige Teile wurden nicht "zu Ihrer eigenen Sicherheit" geändert oder ersetzt. Hier gilt das gleiche. Eine Datenbank soll Ihre Daten speichern, nicht "schützen". Und es ist wichtig, genau die Daten zu speichern, die Sie zurückhaben möchten (das heißt, Ihr alberner base64Versuch ist übrigens auch falsch).

  5. Universelle "Clean'em All" -Desinfektionsfunktion.
    Eine solche Funktion hätte es einfach nie geben dürfen. Erstens gibt es zu viele verschiedene Kontexte / Medien, in denen unsere Daten verwendet werden könnten (SQL-Abfrage, HTML-Code, JS-Code, JSON-Zeichenfolge usw. usw. usw.), die jeweils unterschiedliche Formatierungen erfordern. Sie können also einfach nicht im Voraus sagen, welche Art von Formatierung / Bereinigung erforderlich ist. Darüber hinaus gibt es auch in einem Medium unterschiedliche Literale, die wiederum eine eigene Formatierung erfordern. Das macht es einfach unmöglich, eine einzige umfassende Funktion zu erstellen, um sie alle auf einmal zu formatieren, ohne das Risiko, die Quelldaten zu verderben und gleichzeitig riesige Sicherheitslücken zu hinterlassen.

  6. Herausfiltern bösartiger Zeichen und Sätze.
    Dieser ist einfach - es ist eine absolut imaginäre Maßnahme. Es fällt der Reality-Check schwer. Niemand hat es jemals auf einer mehr oder weniger tragfähigen Website verwendet. Nur weil es die Benutzererfahrung beeinträchtigen wird.

  7. Gespeicherte Prozeduren.
    NICHT in der Webentwicklung. Dieser Vorschlag stammt aus einem anderen Bereich, in dem man Datenbankbenutzer auf eine Reihe gespeicherter Prozeduren beschränken möchte. Es ist eher eine allgemeine Sicherheitsmaßnahme, die sich gegen lokale Benutzer richtet, als gegen einen Eindringling. Wie dem auch sei, der Ansatz ist in der Webentwicklung einfach nicht umsetzbar. Es kann nichts zu dem beitragen, was vorbereitete Aussagen bereits bieten, jedoch auf unbequeme und mühsame Weise.

  8. Separate DB-Konten zum Ausführen von SELECT- und DML-Abfragen. Auch dies ist keine Schutzmaßnahme, sondern nur ein [wertloser] Versuch, die Folgen des bereits durchgeführten Angriffs abzuschwächen. Wertlos, nur wegen der SELECT-basierten Injektion als Katastrophe. Und natürlich nutzlos, weil wir bereits durch die richtige Formatierung unserer Abfragen geschützt sind.  

    Anhang 4. ORMs und Query Builder.

Obwohl sich dieser Artikel nur auf die unformatierten SQL-Abfragen bezieht, finden Sie in den modernen Anwendungen möglicherweise keine Spur von ihnen. Weil alle SQL-Anweisungen von einem ORM oder einem Query Builder hinter den Kulissen verborgen werden. Wenn Ihre Bewerbung so ist, haben Sie Glück. Dieser Artikel wird jedoch aus folgenden Gründen nicht weniger wichtig:

  • Zuallererst müssen sogar ORMs und Query Builder von jemandem geschrieben werden. Und vorbereitete Aussagen machen das Schreiben einfacher und sicherer.
  • Apropos ORM: Solange Sie sich an die Methoden halten können, ist alles in Ordnung. Leider kann man es im wirklichen Leben nicht vollständig verwenden - manchmal müssen Sie auch unformatierte Abfragen ausführen. Halten Sie also immer eine gute Bibliothek bereit, die typenbezogene Platzhalter zusammen mit Ihrem ORM unterstützt.
  • Abfrage-Generator verwenden bereits Platzhalter. Sie würden daher nur von einigen neuen Platzhaltertypen profitieren.

TL; DR

  1. Es gibt keine Injektion, sondern nur eine falsch formatierte Abfrage
  2. Um die Formatierung unverletzlich zu machen, müssen wir vorbereitete Anweisungen verwenden
  3. Vorbereitete Anweisungen müssen wesentlich mehr Datentypen unterstützen als einfache Zeichenfolgen und Zahlen
  4. Um die Formatierung nutzbar zu machen, müssen typisierte Platzhalter verwendet werden
  5. Falls die Formatierung nicht zutreffend ist, muss eine White-Listing-Methode verwendet werden.