Every developer already knows SQL injection is dangerous. The harder problem is what you do once you have inherited tens of thousands of lines of code where queries were built by joining strings together, written over ten years before parameterized queries became the obvious default. You cannot rewrite all of it at once, and you cannot leave it as it is either. Here is how we actually worked through it.
We did not start by fixing a single line. We started by building a complete map of every place where something a user typed could reach a query. A scan across the codebase flagged every place a query was built using a value from a form or a web address, and that gave us a ranked list instead of a vague feeling of worry. We ranked each one by how easy it was to reach and how much harm it could do. A page anyone on the internet could hit was a different priority than a report filter only an admin could see.
Treating each risky spot as its own separate fix is slow, and it is inconsistent too, since twenty different developers will each fix the same kind of problem twenty slightly different ways. It worked far better to group the risky spots by pattern, such as a search filter, a way of building a where clause, or a changeable sort order, and fix each pattern once using one shared, tested helper function, then apply that helper everywhere the pattern showed up. This also made code review possible at our scale. A reviewer could check that one pattern was correct and trust it across fifty places that used it, instead of working out correctness fifty separate times.
Parameterized queries solve the problem of values cleanly, things like a name, a number, or a search word. They do not help when a user's input controls a column name or a table name, since no database driver lets you bind those as a parameter. For these spots, the only safe approach is a strict allow list. Check the requested name against a known, fixed list of actual column or table names before it ever reaches the query, and reject anything that does not match exactly.
It is tempting to push low priority spots to the bottom of the list when the user input is really an internal id, or a value from a screen only an admin can see. We learned to check those too, with a lighter pass, because trust inside a company changes over time. A field that only an admin could see today might be open to a wider group of staff next year, and a query that was safe because nobody untrusted could reach it becomes an actual risk the day that assumption quietly stops being true.
Every single fix we made shipped together with a test that proved the original behaviour still worked, meaning the query still returned the right rows, and where we could, a second test that proved the injection path was closed. Without this, a project this large would have looked, to any reviewer, just like a huge unreviewed rewrite, and reviewers were right to be nervous about approving something like that without proof.
We are still working through lower priority spots today, but the core of the system is now solid, and we know exactly how solid because the tests tell us, every time we run them.
Maybeach Tech has run large SQL injection clean up projects across old PHP codebases without breaking anything in production. Get in touch if you are facing a project like this.