SQL injection is older than most engineers reading this. It still ships every year — because ORMs have escape hatches, because string concatenation is convenient, because some teams ship dynamic SQL builders. The fix has been known for 25 years; the discipline still slips.
Parameterized queries — the whole answer
db.query('SELECT * FROM users WHERE id = ?', [user_id]). The driver sends the query and parameters separately; the database treats parameters as data, not SQL. No string concat = no injection. Every language driver supports this; use it.
ORM escape hatches
Most ORMs have .raw() or .query() methods accepting raw SQL. Often combined with string interpolation 'for performance' or 'for dynamic ORDER BY'. Audit these — they're where injection lives in 2026.
Dynamic identifiers
You can't parameterize table names or column names. So ORDER BY ${user_field} is fundamentally unsafe. Whitelist allowed values; reject unknowns. Don't try to escape — there's no portable safe way.
Stored procedures aren't a fix
A vulnerable proc that builds dynamic SQL inside is just as vulnerable. The proc must use parameterized internal queries too.
Detection in CI
CodeQL queries for tainted-data-to-SQL flow. Semgrep rules for string concat in DB calls. Snyk/Sonar dataflow rules. Run on every PR; gate merges.