Una consulta construida pegando cadenas mezcla sus instrucciones con los datos de otro. La base no puede distinguir unas de otros, así que un valor elegido con cuidado deja de ser un valor y pasa a formar parte de la pregunta.

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

El remedio no es escapar

Escapar intenta volver inofensivo un dato peligroso. Las sentencias preparadas hacen que el dato LLEGUE POR SEPARADO, de modo que nunca pueda leerse como instrucción. A la base se le dice primero la pregunta y después los valores.

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
Escapar puede fallar por un juego de caracteres, por una comilla anidada, por un valor que ya pasó por otra capa. Una sentencia preparada no puede fallar de esas maneras, porque el valor nunca toca el texto SQL.

Lo que un marcador no puede ser

Un marcador ocupa el lugar de un VALOR. No puede ocupar el lugar de un nombre de tabla, un nombre de columna ni un sentido de orden: eso forma parte de la pregunta. Si tienen que venir de una entrada, contrástelos con una lista que haya escrito usted.

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

Encuentre las que tiene

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

Cualquier consulta con una superglobal dentro de la cadena es el patrón. Suele haber un puñado y suelen ser antiguas.

Y la segunda línea

Dele al usuario de la aplicación solo lo que necesita: cuatro verbos sobre una única base. Una inyección exitosa a través de una cuenta que no puede borrar una tabla hace muchísimo menos daño que una a través de una cuenta que sí puede.

Un ORM parametriza por usted hasta que usa su ayudante de consulta cruda. Ese ayudante es donde viven las inyecciones en el código moderno.