PREPARE name FROM 'sql' parses a statement once, with ? where values go; EXECUTE name USING @var supplies values that are never parsed; DEALLOCATE PREPARE frees it. The listing replays the tautology through concatenated dynamic SQL, then through a placeholder:
SET @email = "x' OR '1'='1";
SET @sql = CONCAT("SELECT COUNT(*) AS matched FROM customers WHERE email = '", @email, "'");
PREPARE unsafe FROM @sql;
EXECUTE unsafe;
PREPARE lookup FROM 'SELECT COUNT(*) AS matched FROM customers WHERE email = ?';
EXECUTE lookup USING @email;
PREPARE bad FROM 'SELECT COUNT(*) FROM ?';
DEALLOCATE PREPARE lookup;+---------+ | matched | +---------+ | 8 | +---------+ +---------+ | matched | +---------+ | 0 | +---------+ ERROR 1064 (42000) at line 7: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?' at line 1
PREPARE alone protects nothing: unsafe was built from text that already held the attack. The placeholder does, because the statement's shape is fixed before any input exists. It also stands only for a value, never a table, column or ASC/DESC, so take those from a whitelist in code and quote them with backticks (sys.quote_identifier() in SQL). Statements belong to their session and are capped by max_prepared_stmt_count (16,382). PHP uses the binary protocol's COM_STMT_PREPARE instead; Subsections 4.16.5 and 4.16.6 show it through PDO.