In a pull request, look for any query built by joining strings with request data. The fix to ask for is a parameterized query. Then check the odd cases where parameters cannot be used, such as column names and sort order, and confirm the database account has no more rights than it needs.
The primary defenses, in order
The OWASP SQL Injection Prevention Cheat Sheet lists them in this order: prepared statements with parameterized queries, stored procedures written safely, allow list input validation where bind variables are not legal, and escaping, which it strongly discourages because it is fragile and database specific.
For a reviewer, the first item becomes a rule of thumb. If the diff contains a query and a variable inside the same string, stop and ask why.
Quick red flags in a diff
| What you see | Why it matters |
|---|---|
| String concatenation or interpolation into SQL | Input can change the query structure |
| A helper called raw, execute or query with a built string | Bypasses the safe path of the framework |
| User input chosen as table, column or sort order | Parameters cannot bind identifiers |
| Dynamic SQL inside a stored procedure | Procedures are not automatically safe |
| New database user with broad rights | A bug becomes a bigger breach |
The cases where parameters do not work
Identifiers such as table names, column names and ORDER BY cannot be bound as parameters. OWASP suggests taking identifiers from code rather than from user input. If a user must choose one, map the input to a fixed set of known names, for example in a switch statement, and reject everything else. For sort direction, convert the input to a boolean and pick ASC or DESC from that.
A small illustration of the review comment you might leave:
# risky: the column name comes straight from the request
query = "SELECT * FROM orders ORDER BY " + request.args["sort"]
# safer: the request only selects from known choices
allowed = {"date": "created_at", "total": "total_amount"}
column = allowed.get(request.args.get("sort"), "created_at")
query = "SELECT * FROM orders ORDER BY " + column
The second version still builds a string, but every part of it comes from your own code.
Do not trust validation alone
OWASP notes that allow list validation is a secondary defense in all cases, even with bind variables, and a primary defense only when nothing else is possible. Validated input is not automatically safe to concatenate into SQL. Treat validation as a layer, not the fix.
Check the account behind the query
The cheat sheet is blunt: do not give application accounts DBA or admin type access. Use a separate account per application and limit table access with views or procedures where it makes sense. When a pull request adds a new connection string or migration role, read the grants. An injection bug under a read only account exposes less than the same bug under an owner account.
Try it yourself
Run the code scan in the demo and look at how a finding points to the exact line where input reaches a query.
Click through it at your own pace, or open it full screen.
Where Safeguard fits
Safeguard scans dependencies and builds an SBOM, and you can try the free scan at gold.safeguard.sh/scan. Query construction in your own code is a review and testing job.
Quick check
Is escaping user input as good as parameterization?
No. OWASP strongly discourages escaping as a primary defense because it is fragile and differs between databases.
Are stored procedures always safe?
No. They are equally effective only if written without unsafe dynamic SQL, and execute rights can push applications toward over-privileged accounts.
Sources
Last reviewed: 19 October 2026.