742 Stimmen

Sind PDO Prepared Statements ausreichend, um SQL Injection zu verhindern?

Nehmen wir an, ich habe einen Code wie diesen:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

In der PDO-Dokumentation steht:

Die Parameter für vorbereitete Anweisungen müssen nicht in Anführungszeichen gesetzt werden; der Treiber übernimmt dies für Sie.

Ist das wirklich alles, was ich tun muss, um SQL-Injections zu vermeiden? Ist es wirklich so einfach?

Sie können MySQL übernehmen, wenn es einen Unterschied macht. Außerdem bin ich wirklich nur an der Verwendung von vorbereiteten Anweisungen gegen SQL-Injection interessiert. In diesem Zusammenhang interessieren mich XSS oder andere mögliche Schwachstellen nicht.

5 Stimmen

Besserer Ansatz 7. Nummer Antwort stackoverflow.com/questions/134099/

919voto

ircmaxell Punkte 159431

Die kurze Antwort lautet NO Die PDO-Vorbereitungen schützen Sie nicht vor allen möglichen SQL-Injection-Angriffen. Für bestimmte obskure Randfälle.

Ich passe mich an diese Antwort um über PDO zu sprechen...

Die lange Antwort ist nicht so einfach. Sie basiert auf einem Angriff hier demonstriert .

Die Attacke

Beginnen wir also damit, den Angriff zu zeigen...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

Unter bestimmten Umständen wird mehr als 1 Zeile zurückgegeben. Sehen wir uns an, was hier vor sich geht:

  1. Auswählen eines Zeichensatzes

    $pdo->query('SET NAMES gbk');

    Damit dieser Angriff funktioniert, müssen wir die Kodierung, die der Server auf der Verbindung erwartet, sowohl für die Kodierung ' wie bei ASCII, d.h. 0x27 und ein Zeichen zu haben, dessen letztes Byte ein ASCII-Zeichen ist \ d.h. 0x5c . Wie sich herausstellt, werden in MySQL 5.6 standardmäßig 5 solcher Kodierungen unterstützt: big5 , cp932 , gb2312 , gbk y sjis . Wir wählen aus gbk hier.

    Nun ist es sehr wichtig, die Verwendung von SET NAMES hier. Damit wird der Zeichensatz festgelegt AUF DEM SERVER . Es gibt noch einen anderen Weg, aber dazu kommen wir noch früh genug.

  2. Die Nutzlast

    Die Nutzlast, die wir für diese Injektion verwenden werden, beginnt mit der Byte-Sequenz 0xbf27 . Unter gbk ist ein ungültiges Multibyte-Zeichen; in latin1 ist es die Zeichenkette ¿' . Beachten Sie, dass in latin1 und gbk , 0x27 ist für sich genommen eine wörtliche ' Charakter.

    Wir haben diese Nutzlast gewählt, weil wir bei einem Aufruf von addslashes() darauf, würden wir ein ASCII \ d.h. 0x5c , vor dem ' Zeichen. Also würden wir am Ende 0xbf5c27 die in gbk ist eine zweistellige Zeichenfolge: 0xbf5c gefolgt von 0x27 . Oder mit anderen Worten, ein gültig Zeichen, gefolgt von einem nicht abgeschnittenen ' . Aber wir benutzen nicht addslashes() . Also weiter zum nächsten Schritt...

  3. $stmt->execute()

    Das Wichtigste ist, dass PDO standardmäßig NO echte vorbereitete Erklärungen abgeben. Es emuliert sie (für MySQL). Deshalb baut PDO intern die Abfragezeichenfolge auf, indem es mysql_real_escape_string() (die MySQL-C-API-Funktion) für jeden gebundenen Zeichenfolgenwert.

    Der C-API-Aufruf an mysql_real_escape_string() unterscheidet sich von addslashes() dass es den Zeichensatz der Verbindung kennt. Daher kann es die Umformung für den Zeichensatz, den der Server erwartet, korrekt durchführen. Bis zu diesem Punkt denkt der Client jedoch, dass wir immer noch mit latin1 für die Verbindung, weil wir ihm nie etwas anderes gesagt haben. Wir haben dem Server wir verwenden gbk aber die Kunde denkt immer noch, es sei latin1 .

    Daher die Aufforderung an mysql_real_escape_string() fügt den Backslash ein, und wir haben eine frei hängende ' Zeichen in unserem "entkommenen" Inhalt! In der Tat, wenn wir uns ansehen würden $var im gbk Zeichensatz, würden wir sehen:

    ' OR 1=1 /\*

    Das ist genau das, was der Angriff erfordert.

  4. Die Abfrage

    Dieser Teil ist nur eine Formalität, aber hier ist die gerenderte Abfrage:

    SELECT * FROM test WHERE name = '' OR 1=1 /*' LIMIT 1

Herzlichen Glückwunsch, Sie haben gerade erfolgreich ein Programm mit PDO Prepared Statements angegriffen...

Die einfache Lösung

Es ist erwähnenswert, dass Sie dies verhindern können, indem Sie emulierte vorbereitete Anweisungen deaktivieren:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Dies wird in der Regel zu einer echten vorbereiteten Anweisung führen (d. h. die Daten werden in einem von der Abfrage getrennten Paket übermittelt). Beachten Sie jedoch, dass PDO stillschweigend Fallback um Statements zu emulieren, die MySQL nicht nativ vorbereiten kann: Diejenigen, die es kann, sind aufgelistet im Handbuch, aber achten Sie darauf, dass Sie die richtige Serverversion auswählen).

Die richtige Lösung

Das Problem hier ist, dass wir die C-API's nicht aufgerufen haben mysql_set_charset() anstelle von SET NAMES . Wenn wir das täten, wäre alles in Ordnung, vorausgesetzt, wir verwenden eine MySQL-Version seit 2006.

Wenn Sie eine frühere Version von MySQL verwenden, dann ist ein error en mysql_real_escape_string() bedeutete, dass ungültige Multibyte-Zeichen, wie die in unserer Nutzlast, als einzelne Bytes behandelt wurden, um sie zu entschlüsseln selbst wenn der Kunde korrekt über die Verbindungskodierung informiert wurde und so würde dieser Angriff immer noch erfolgreich sein. Der Fehler wurde in MySQL behoben 4.1.20 , 5.0.22 y 5.1.11 .

Aber das Schlimmste ist, dass PDO nicht die C-API für mysql_set_charset() bis 5.3.6, also in früheren Versionen kann nicht Verhindern Sie diesen Angriff für jeden möglichen Befehl! Es ist jetzt als ein DSN-Parameter die verwendet werden sollten anstelle von SET NAMES ...

Die rettende Gnade

Wie eingangs erwähnt, muss die Datenbankverbindung mit einem angreifbaren Zeichensatz kodiert sein, damit dieser Angriff funktioniert. utf8mb4 es nicht anfällig und kann dennoch unterstützen jede Unicode-Zeichen: Sie könnten sich also dafür entscheiden, stattdessen dieses Zeichen zu verwenden - es ist aber erst seit MySQL 5.5.3 verfügbar. Eine Alternative ist utf8 die auch nicht anfällig und kann den gesamten Unicode unterstützen Basic Multilingual Plane .

Alternativ dazu können Sie die Option NO_BACKSLASH_ESCAPES SQL-Modus, der (unter anderem) die Funktionsweise von mysql_real_escape_string() . Wenn dieser Modus aktiviert ist, 0x27 wird ersetzt durch 0x2727 statt 0x5c27 und damit der Fluchtprozess kann nicht gültige Zeichen in einer der gefährdeten Kodierungen zu erzeugen, wo sie vorher nicht existierten (d. h. 0xbf27 ist noch 0xbf27 usw.), so dass der Server die Zeichenkette immer noch als ungültig zurückweisen wird. Siehe jedoch @eggyal's Antwort für eine andere Sicherheitslücke, die durch die Verwendung dieses SQL-Modus entstehen kann (wenn auch nicht mit PDO).

Sichere Beispiele

Die folgenden Beispiele sind sicher:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Weil der Serer erwartet, dass utf8 ...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Denn wir haben den Zeichensatz richtig eingestellt, so dass der Client und der Server übereinstimmen.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir die emulierten vorbereiteten Erklärungen ausgeschaltet haben.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Weil wir den Zeichensatz richtig eingestellt haben.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Weil MySQLi ständig echte vorbereitete Anweisungen ausführt.

Einpacken

Wenn Sie:

  • Verwenden Sie moderne Versionen von MySQL (spätes 5.1, alle 5.5, 5.6, etc) UND PDOs DSN-Zeichensatzparameter (in PHP 5.3.6)

O

  • Verwenden Sie keinen anfälligen Zeichensatz für die Verbindungskodierung (Sie verwenden nur utf8 / latin1 / ascii / etc)

O

  • Aktivieren Sie NO_BACKSLASH_ESCAPES SQL-Modus

Sie sind 100% sicher.

Andernfalls sind Sie angreifbar. auch wenn Sie PDO Prepared Statements verwenden...

Apéndice

Ich habe langsam an einem Patch gearbeitet, um die Standardeinstellung so zu ändern, dass sie für eine zukünftige Version von PHP nicht mehr emuliert wird. Das Problem, auf das ich stoße, ist, dass eine Menge Tests abbrechen, wenn ich das tue. Ein Problem ist, dass emulierte prepares nur Syntaxfehler beim Ausführen ausgeben, aber echte prepares geben Fehler beim Vorbereiten aus. Das kann also zu Problemen führen (und ist Teil des Grundes, warum die Tests nicht funktionieren).

60 Stimmen

Dies ist die beste Antwort, die ich gefunden habe können Sie einen Link für weitere Referenzen angeben?

0 Stimmen

Ich glaube, die "rettende Gnade" war diese Korrektur von MySQL 5.0.22: dev.mysql.com/doc/relnotes/mysql/5.0/de/news-5-0-22.html

1 Stimmen

@nicogawenda: Das war ein anderer Fehler. Vor 5.0.22, mysql_real_escape_string behandelte Fälle, in denen die Verbindung korrekt auf BIG5/GBK eingestellt war, nicht richtig. Also eigentlich sogar der Aufruf von mysql_set_charset() auf mysql < 5.0.22 wäre anfällig für diesen Fehler! Also nein, dieser Beitrag ist immer noch auf 5.0.22 anwendbar (weil mysql_real_escape_string nur den Zeichensatz für Aufrufe von mysql_set_charset() , um die es in diesem Beitrag geht, zu umgehen)...

523voto

Joel Coehoorn Punkte 377088

Vorbereitete Anweisungen / parametrisierte Abfragen sind im Allgemeinen ausreichend, um zu verhindern, dass 1. Auftrag Injektion auf diese Aussage * . Wenn Sie an anderer Stelle in Ihrer Anwendung ungeprüfte dynamische SQL verwenden, sind Sie immer noch anfällig für 2. Bestellung Injektion.

Injektion 2. Ordnung bedeutet, dass die Daten einmal durch die Datenbank gelaufen sind, bevor sie in eine Abfrage aufgenommen wurden, und ist viel schwieriger zu bewerkstelligen. Nach bestem Wissen und Gewissen sieht man so gut wie nie echte Angriffe 2. Ordnung, da es für Angreifer in der Regel einfacher ist, sich ihren Weg durch Social Engineering zu bahnen, aber manchmal tauchen Bugs 2. ' Zeichen oder ähnliches.

Sie können einen Injektionsangriff 2. Ordnung durchführen, wenn Sie einen Wert in einer Datenbank speichern können, der später als Literal in einer Abfrage verwendet wird. Nehmen wir an, Sie geben die folgenden Informationen als Ihren neuen Benutzernamen ein, wenn Sie ein Konto auf einer Website erstellen (für diese Frage wird eine MySQL-DB angenommen):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Wenn es keine anderen Einschränkungen für den Benutzernamen gibt, würde eine vorbereitete Anweisung immer noch sicherstellen, dass die oben eingebettete Abfrage zum Zeitpunkt des Einfügens nicht ausgeführt wird, und den Wert korrekt in der Datenbank speichern. Stellen Sie sich jedoch vor, dass die Anwendung zu einem späteren Zeitpunkt Ihren Benutzernamen aus der Datenbank abruft und die Zeichenkettenverkettung verwendet, um diesen Wert in eine neue Abfrage aufzunehmen. Sie könnten das Passwort einer anderen Person sehen. Da die ersten Namen in der Benutzertabelle in der Regel Admins sind, haben Sie vielleicht auch gerade die Farm verraten (ein Grund mehr, Kennwörter nicht im Klartext zu speichern).

Wir sehen also, dass vorbereitete Anweisungen für eine einzelne Abfrage ausreichen, aber für sich genommen sind sie pas Sie reichen nicht aus, um eine gesamte Anwendung vor Sql-Injection-Angriffen zu schützen, da sie keinen Mechanismus haben, um zu erzwingen, dass alle Zugriffe auf eine Datenbank innerhalb einer Anwendung sicheren Code verwenden. Als Teil eines guten Anwendungsdesigns - das Praktiken wie Code-Review oder statische Analyse oder die Verwendung einer ORM-, Daten- oder Serviceschicht, die dynamisches Sql-Injection einschränkt, umfassen kann - sind sie jedoch ausreichend. vorbereitete Erklärungen son das wichtigste Werkzeug zur Lösung des Sql Injection-Problems. Wenn Sie die Grundsätze eines guten Anwendungsdesigns befolgen, d. h. den Datenzugriff vom Rest Ihres Programms trennen, ist es einfach, die korrekte Verwendung der Parameter in jeder Abfrage zu erzwingen oder zu überprüfen. In diesem Fall wird Sql-Injection (sowohl erster als auch zweiter Ordnung) vollständig verhindert.


<sup>* </sup>Es stellt sich heraus, dass MySql/PHP sind (okay, waren) nur dumm über die Handhabung von Parametern, wenn breite Zeichen beteiligt sind, und es gibt immer noch eine <em>selten </em>Fall, der in der <a href="https://stackoverflow.com/a/12202218/3043">andere hoch bewertete Antwort hier </a>die eine Injektion durch eine parametrisierte Abfrage hindurch ermöglichen kann.

6 Stimmen

Das ist interessant. Ich war mir der Unterschiede zwischen 1. und 2. Ordnung nicht bewusst. Können Sie etwas ausführlicher erklären, wie die 2.

197 Stimmen

Wenn ALLE Ihre Abfragen parametrisiert sind, sind Sie auch vor Injektionen 2. Ordnung geschützt. Injektion 1. Ordnung ist das Vergessen, dass die Benutzerdaten nicht vertrauenswürdig sind. Injektion 2. Ordnung bedeutet, dass man vergisst, dass die Datenbankdaten nicht vertrauenswürdig sind (weil sie ursprünglich vom Benutzer stammen).

6 Stimmen

Danke, CJM. Ich fand auch diesen Artikel hilfreich, der die Injektionen 2. Ordnung erklärt: codeproject.com/KB/database/SqlInjectionAttacks.aspx

45voto

Tower Punkte 92943

Nein, das sind sie nicht immer.

Es hängt davon ab, ob Sie zulassen, dass Benutzereingaben in die Abfrage selbst eingefügt werden. Zum Beispiel:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

wäre anfällig für SQL-Injections und die Verwendung von Prepared Statements in diesem Beispiel würde nicht funktionieren, da die Benutzereingabe als Bezeichner und nicht als Daten verwendet wird. Die richtige Antwort wäre hier die Verwendung einer Art von Filterung/Validierung wie:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Hinweis: Sie können PDO nicht verwenden, um Daten zu binden, die außerhalb der DDL (Data Definition Language) liegen, d. h. dies funktioniert nicht:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

Der Grund, warum dies nicht funktioniert, ist folgender DESC y ASC sind nicht Daten . PDO kann nur entkommen für Daten . Zweitens: Sie können nicht einmal die ' Anführungszeichen. Die einzige Möglichkeit, eine benutzerdefinierte Sortierung zuzulassen, besteht darin, manuell zu filtern und zu überprüfen, dass es sich entweder um DESC o ASC .

13 Stimmen

Übersehe ich etwas hier, aber ist nicht der ganze Sinn von vorbereiteten Anweisungen zu vermeiden, Sql wie eine Zeichenfolge zu behandeln? Würde nicht etwas wie $dbh->prepare('SELECT * FROM :tableToUse where username = :username'); Ihr Problem umgehen?

4 Stimmen

@RobForrest ja Sie fehlen :). Die Daten, die Sie binden, funktionieren nur für DDL (Data Definition Language). Sie brauchen die Anführungszeichen und die richtige Escaping-Funktion. Wenn Sie Anführungszeichen für andere Teile der Abfrage setzen, wird diese mit hoher Wahrscheinlichkeit zerstört. Zum Beispiel, SELECT * FROM 'table' kann falsch sein, wie es sein sollte SELECT * FROM `table` oder ohne Backsticks. Dann einige Dinge wie ORDER BY DESC donde DESC vom Benutzer kommt, kann nicht einfach ausgeblendet werden. Die praktischen Szenarien sind also ziemlich unbegrenzt.

11 Stimmen

Ich frage mich, wie 6 Personen einen Kommentar hochstufen konnten, in dem eine eindeutig falsche Verwendung einer vorbereiteten Erklärung vorgeschlagen wurde. Hätten sie es auch nur einmal versucht, hätten sie sofort erkannt, dass die Verwendung benannter Parameter anstelle eines Tabellennamens nicht funktioniert.

33voto

PeeHaa Punkte 68988

Nein, das ist nicht genug (in bestimmten Fällen)! Standardmäßig verwendet PDO emulierte vorbereitete Anweisungen, wenn MySQL als Datenbanktreiber verwendet wird. Sie sollten emulierte Prepared Statements immer deaktivieren, wenn Sie MySQL und PDO verwenden:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Ein weiterer Punkt, der immer beachtet werden sollte, ist die Einstellung der richtigen Kodierung der Datenbank:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Siehe auch diese verwandte Frage: Wie kann ich SQL-Injection in PHP verhindern?

Beachten Sie auch, dass es sich hierbei nur um die Datenbankseite handelt, die Sie bei der Anzeige der Daten noch selbst überwachen müssen. Z.B. durch die Verwendung von htmlspecialchars() wieder mit der richtigen Kodierung und Zitierweise.

29voto

troelskn Punkte 110542

Ja, das ist ausreichend. Die Art und Weise, wie Injektionsangriffe funktionieren, besteht darin, einen Interpreter (die Datenbank) dazu zu bringen, etwas, das eigentlich Daten sein sollten, so auszuwerten, als ob es Code wäre. Dies ist nur möglich, wenn Sie Code und Daten im selben Medium vermischen (z. B. wenn Sie eine Abfrage als String konstruieren).

Bei parametrisierten Abfragen werden der Code und die Daten getrennt gesendet, so dass es niemals möglich sein, ein Loch darin zu finden.

Sie können jedoch immer noch für andere Injektionsangriffe anfällig sein. Wenn Sie zum Beispiel die Daten in einer HTML-Seite verwenden, können Sie XSS-Angriffen ausgesetzt sein.

11 Stimmen

"Niemals" ist Weg Übertreibung bis hin zur Irreführung. Wenn Sie vorbereitete Anweisungen falsch verwenden, ist das nicht viel besser, als sie überhaupt nicht zu verwenden. (Natürlich macht eine "vorbereitete Anweisung", in die Benutzereingaben eingefügt wurden, den Zweck zunichte... aber ich habe es tatsächlich schon gesehen. Und vorbereitete Anweisungen können keine Bezeichner (Tabellennamen usw.) als Parameter verarbeiten). Hinzu kommt, dass einige der PDO-Treiber nachbilden. vorbereitete Anweisungen, und es besteht die Möglichkeit, dass sie dies falsch tun (z. B. durch halbherziges Parsen des SQL). Kurz gesagt: Gehen Sie nie davon aus, dass es so einfach ist.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X