Eine aus zusammengeklebten Zeichenketten gebaute Abfrage vermischt Ihre Anweisungen mit fremden Daten. Die Datenbank kann beides nicht auseinanderhalten - ein sorgfältig gewählter Wert hört also auf, ein Wert zu sein, und wird Teil der Frage.

// wrong, and it is wrong even if you escape
$sql = "SELECT * FROM users WHERE email = '" . $email . "'";

Die Abhilfe ist nicht Escaping

Escaping versucht, gefährliche Daten ungefährlich zu machen. Prepared Statements lassen die Daten GETRENNT ANKOMMEN, sodass sie überhaupt nie als Anweisungen gelesen werden können. Der Datenbank wird zuerst die Frage gesagt und danach die Werte.

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
Escaping kann an einem Zeichensatz scheitern, an einem verschachtelten Anführungszeichen, an einem Wert, der schon durch eine andere Schicht gelaufen ist. Ein Prepared Statement kann auf diese Weisen nicht scheitern, weil der Wert den SQL-Text nie berührt.

Was ein Platzhalter nicht sein kann

Ein Platzhalter steht für einen WERT. Er kann nicht für einen Tabellennamen, einen Spaltennamen oder eine Sortierrichtung stehen - die sind Teil der Frage. Müssen sie aus Eingaben kommen, prüfen Sie sie gegen eine Liste, die Sie selbst geschrieben haben.

$allowed = ['created_at', 'total', 'status'];
$sort = in_array($in, $allowed, true) ? $in : 'created_at';
$sql = "SELECT * FROM orders ORDER BY $sort DESC";

Finden, welche Sie haben

grep -rn "SELECT.*\$_\(GET\|POST\|REQUEST\)" --include="*.php" . | head

Jede Abfrage mit einer Superglobalen mitten in der Zeichenkette ist das Muster. Meist sind es eine Handvoll, und meist sind sie alt.

Und die zweite Linie

Geben Sie dem Anwendungsbenutzer nur, was er braucht - vier Verben auf einer Datenbank. Eine geglückte Injection über ein Konto, das keine Tabelle löschen kann, richtet weit weniger an als eine über ein Konto, das es kann.

Ein ORM parametrisiert für Sie - bis Sie seinen Helfer für Rohabfragen benutzen. In diesem Helfer wohnen die Injections in modernem Code.