Una query costruita incollando stringhe mescola le sue istruzioni con i dati di qualcun altro. Il database non riesce a distinguere gli uni dagli altri: così un valore scelto con cura smette di essere un valore e diventa parte della domanda.

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

Il rimedio non è l'escaping

L'escaping cerca di rendere innocuo un dato pericoloso. I prepared statement fanno ARRIVARE il dato SEPARATAMENTE, così non può mai essere letto come istruzione. Al database si dice prima la domanda e poi i valori.

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
L'escaping può andare storto per un set di caratteri, per un apice annidato, per un valore già passato attraverso un altro strato. Un prepared statement non può andare storto in quei modi, perché il valore non tocca mai il testo SQL.

Che cosa un segnaposto non può essere

Un segnaposto sta al posto di un VALORE. Non può stare al posto di un nome di tabella, di un nome di colonna o di un verso di ordinamento: quelli sono parte della domanda. Se devono arrivare dall'input, li confronti con un elenco scritto da lei.

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

Trovi quelle che ha

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

Qualsiasi query con una superglobale dentro la stringa è il pattern. Di solito ce n'è una manciata e di solito sono vecchie.

E la seconda linea

Dia all'utente dell'applicazione soltanto ciò che gli serve: quattro verbi su un solo database. Un'injection riuscita attraverso un account che non può cancellare una tabella fa assai meno danno di una attraverso un account che può.

Un ORM parametrizza al posto suo finché lei non usa il suo helper per le query grezze. È in quell'helper che, nel codice moderno, abitano le injection.