Prevent SQL injection in PHP by binding every dynamic value through a prepared statement, checking any dynamic identifier such as a sort direction against a fixed allow list, connecting with a least-privilege database account, and keeping database error detail out of responses. Escaping strings by hand is the fallback, not the plan.
What does a prepared statement change?
A prepared statement sends the SQL and the data separately. The OWASP SQL Injection Prevention Cheat Sheet puts it this way: the database will always distinguish between code and data, regardless of what user input is supplied. The PHP manual agrees that prepared statements are the primary defence and tells you to replace every dynamic literal in WHERE, SET and VALUES clauses with a placeholder.
A constructed vulnerable pattern: a page builds a query by joining a request parameter into a string. A value containing a quote and extra SQL changes the meaning of the statement. The fix does not involve filtering the quote. It removes the user value from the SQL text altogether. With PDO, the code prepares a statement with a question mark or named placeholder, then passes the value to execute.
What can't placeholders do?
This is where many "parameterised" codebases still fail. The PHP manual states that parameter binding works for data only. Other dynamic parts of a query, such as a sort direction or a column name, cannot be bound.
For those, the manual's example maps the request value to a safe constant in code, for instance choosing between ASC and DESC with a conditional rather than inserting the user's string. OWASP says the same: use an allow list, map the input to a fixed set of expected values, and reject anything else. OWASP's preferred option is to redesign so the user never chooses identifiers directly.
If a user can pick a column to sort by, keep an array of permitted column names and look up the request value in it. If it is not in the array, use a default.
What about escaping and stored procedures?
OWASP strongly discourages escaping user input as a primary defence because it is database-specific, fragile and cannot guarantee protection. The PHP manual lists escaping only as a fallback where binding is unavailable, and advises against addslashes except in very specific environments.
Stored procedures can be about as effective as prepared statements if they do not build unsafe dynamic SQL inside. OWASP also warns they can raise risk indirectly, because needing execute rights leads teams to grant wider roles than necessary.
How does least privilege limit the damage?
Both sources recommend it. The PHP manual says never to connect as a superuser or database owner, and to use customised accounts with minimal privileges. OWASP is blunt: do not assign DBA or admin type access to application accounts, give each application its own account, and use views to restrict readable columns where it helps. Run the database server itself under a non-root account.
So if an injection slips through, the attacker gets one application's limited rights, not the whole server.
What else belongs in the checklist?
- Validate input types: use
ctype_digitoris_numericfor numeric ids, or cast, as the manual describes. - Keep database-specific error text and schema details out of responses. Log them privately instead.
- Store only hashes of passwords.
- Use an ORM or query builder carefully. They bind values for you, but a raw fragment passed into one is the same as concatenation.
What to do
- Search the codebase for query strings built with concatenation or interpolation, and replace them with placeholders.
- Create an allow list for every dynamic identifier, and default when the input is not on it.
- Audit raw SQL fragments passed into your ORM or query builder.
- Give each application a dedicated least-privilege database user.
- Turn off detailed database errors in production and log them privately.
- Add static analysis to CI to flag string-built queries. The sast-code-scan demo shows the idea.
See the full walkthrough at /demos/sast-code-scan.
Quick check
Can I bind a column name or ORDER BY direction as a parameter?
No. Binding covers data only. Check those values against an allow list and use a fixed constant in the SQL.
Is mysqli_real_escape_string enough?
It is a fallback. OWASP discourages relying on escaping because it is database-specific and fragile. Use prepared statements.
Sources
Last reviewed: 18 October 2026.