Prevent SQL injection by keeping data and SQL separate. Use parameterized queries (prepared statements) for every value, validate against an allow list for the parts that cannot be parameters, such as column names, and give the database account the least privilege it needs. OWASP strongly discourages relying on escaping alone.
What is the underlying flaw?
SQL injection happens when an application builds a query by joining strings, so user input can change the meaning of the statement rather than just supply a value. The database cannot tell which part was written by the developer and which part came from the visitor. The fix is to make that distinction explicit, which is what parameters do.
What does OWASP list as the primary defenses?
The OWASP SQL Injection Prevention Cheat Sheet names four options and ranks them.
Option 1 is prepared statements with parameterized queries. The query text is defined first with placeholders, and the values are supplied separately, so input is never parsed as SQL. Option 2 is stored procedures, which OWASP says are not always safe but work like parameterized queries when implemented safely. Option 3 is allow-list input validation, for the pieces that bind variables cannot handle. Option 4, escaping all user-supplied input, is labeled strongly discouraged.
OWASP adds least privilege as a further layer, including using database views to narrow what an account can read.
What does the fix look like?
In Python with a typical database driver, the unsafe and safe forms differ by one idea:
# Unsafe: value is joined into the SQL text
cur.execute("SELECT id, email FROM users WHERE email = '" + email + "'")
# Safe: value travels separately from the SQL text
cur.execute("SELECT id, email FROM users WHERE email = %s", (email,))
Placeholder syntax differs by driver, and the same principle holds in every language. Object-relational mappers use parameters too, until a developer drops to a raw query helper and concatenates a string. Those helpers are where reviews should look first.
What about column names and sort order?
Placeholders bind values, not identifiers. A feature like "sort by any column" cannot use a parameter for the column. The answer is an allow list: map the user's choice to a fixed set of column names that you wrote, and reject everything else. The same applies to table names and the ascending or descending keyword.
Why is escaping not enough?
OWASP says escaping is very database specific and fragile compared to the other defenses, and that it cannot guarantee the approach prevents all SQL injection in all situations. Each database has its own rules, and a single missed case becomes a hole. For new code or low risk tolerance, OWASP says to build with parameterized queries, stored procedures or an ORM that builds queries for you.
Where does least privilege help?
It limits the damage when something else fails. A web application account that can only select from three tables and insert into one cannot drop tables or read the payments schema. Use separate accounts for read-only reporting and for the application. Do not run the application as the database owner.
What does a real finding look like?
Constructed example: a support portal has a search box that builds a query from the typed text. The test environment has 40 rows, so nothing looks wrong. In production, the account that runs the query also has access to the customer table, so a crafted search string could return rows nobody intended. Parameterizing the query fixes the root cause, and narrowing the account's rights would have limited the exposure even before the fix.
How to check or apply this
- Search the codebase for query strings built with concatenation or string formatting, including raw query helpers in ORMs.
- Replace each with a parameterized call, and add a test.
- For dynamic identifiers, introduce an allow list and reject unknown values with a clear error.
- Review stored procedures for dynamic SQL built from arguments.
- Create a dedicated database user per application with only the required permissions. Revoke ownership rights.
- Turn on database error handling that logs details server side and returns a generic message to the client.
- Add static analysis to CI and a dynamic scan of staging, since each catches things the other misses.
- Log and alert on database errors that spike, which often signal probing.
Quick check
Are stored procedures a guaranteed fix?
No. OWASP says they help when implemented safely, but a procedure that builds dynamic SQL from its arguments is as vulnerable as application code.
Can I sanitize input by removing quote characters?
That is fragile. OWASP strongly discourages escaping as the main defense and cannot guarantee it blocks every case. Use parameterized queries and allow lists.
Sources
Last reviewed: 18 October 2026.