SQL injection has been well understood for twenty-five years and remains among the most damaging vulnerabilities found in production systems. Not because the fix is hard - it is one line - but because modern frameworks prevent it so thoroughly by default that when someone does write raw SQL, they are usually doing so in unusual circumstances, without the habits that would protect them, and in a part of the codebase nobody reviews closely.
The mechanism is worth stating precisely, because the mitigation follows from it. When you build a query by concatenating a string, the database receives one indivisible instruction and has no way to know which parts came from you and which from a user. Parameterised queries send the statement and the values separately, so the database knows the values are data and can never interpret them as commands. That separation - not escaping, not filtering - is the actual fix.
Escaping and blocklists are the tempting wrong answer. Stripping quotes, filtering the word DROP, rejecting semicolons: each of these can be bypassed, and the bypasses are well documented. Any defence built on recognising malicious input is a defence you must maintain against every technique anyone invents. Parameterisation is not a filter and cannot be bypassed, which is why it is the only recommendation worth making.
The places it still appears are consistent. Dynamic ORDER BY and column names, which cannot be parameterised because they are identifiers rather than values - the correct approach is an allowlist mapping user input to known column names, never interpolation. Search features building complex WHERE clauses conditionally. Reporting and admin tools where someone needed something the ORM would not express. Migrations and one-off scripts written under time pressure. Stored procedures that build dynamic SQL internally, which look safe from the application side and are not.
Validation is a separate concern from injection and worth doing on its own terms. Validate that inputs are the right type, length and format at the boundary, and reject rather than sanitise - a sanitiser that silently alters input creates its own surprises. But be clear that validation is defence in depth: a validated string is still data, and it still belongs in a parameter rather than in a concatenated query.
For assurance rather than hope, add a static analysis step to your pipeline that flags string concatenation into query methods. It is cheap, it catches the case where someone writes raw SQL in a hurry, and it converts this from a matter of everyone remembering into a matter of the build failing. Then use a database account with only the permissions the application needs - no schema modification, no access to tables it never reads - so that if something does get through, what it can do is bounded.