Keep SQL structure fixed, bind data through the driver’s supported API, constrain structural choices and verify authorization independently.

Prevent SQL injection by keeping database instructions separate from data values. Use the database driver or query builder’s supported parameter-binding mechanism rather than constructing SQL by joining user input into a string. Where users choose a structural option such as sorting, map that choice to a deliberately allowed fixed expression.
This is defensive guidance for software you own or are authorized to maintain. It provides no attack payloads and does not ask you to probe production. The goal is a reviewable query boundary, with ordinary application validation and authorization still in place.
Find where the query is actually assembled
Start with one reader-facing task: searching articles, filtering a member list or opening an authorized report. Trace the submitted value through the handler, query builder and driver. A friendly method named safeSearch is not evidence if its implementation later concatenates raw input into SQL.
Include indirect sources. A value may come from an import, scheduled job or previously saved record rather than the current form. Its origin affects validation requirements but does not turn it into trusted SQL instructions. Review every path that reaches the same query.
For a WordPress site, identify whether core APIs, a plugin or custom code owns the operation. Keep production administration read-only during investigation. Review the supported framework API and version with the developer before proposing a specific code change.
Bind the value, not a finished SQL string
OWASP recommends prepared statements with parameterized queries as a primary defense. The structure is defined separately from the values supplied at execution. Escaping every input manually is a discouraged substitute, and a stored procedure is not automatically safe if it builds dynamic SQL unsafely inside.
The distinction is visible in a harmless read-only example. The following illustrates Python sqlite3 parameter binding for a fictional catalogue; it is not a complete application or a portable driver recipe.
query = "SELECT title FROM articles WHERE category_id = ?"
rows = connection.execute(query, (chosen_category_id,))
Python’s sqlite3 documentation defines placeholder binding and the expected parameter forms. The value is supplied through the API separately, not inserted through string formatting before execute is called. Use the equivalent maintained API for your actual driver.
Keep data types and product rules clear. Binding a value does not establish that a category exists, a date range is acceptable or a caller may read its results. Validate those requirements before or as part of the authorized query.
A fictional catalogue search
A fictional publisher offers category and sort controls. Categories are stored as stable identifiers. The developer binds the selected category as a value and checks it belongs to the supported catalogue. The sort control offers only newest or title.
The implementation maps newest to its fixed date-order expression and title to its fixed title-order expression. The browser’s label never becomes a raw database instruction. An unknown sort choice receives the application’s documented validation response instead of being pasted into the query.
In a staging review, the team checks ordinary category identifiers, missing values, unsupported sort labels, a long search phrase and punctuation in text. It verifies expected results and clear errors without storing real member data or attempting an attack. These are fictional proposed checks, not reported test results.
| Input meaning | Defensive treatment | Separate responsibility |
|---|---|---|
| Search text | Bind as a value | Length, search behavior and result limits |
| Category identifier | Bind and validate supported meaning | Visibility and account permission |
| Sort field or direction | Map to fixed allowed structure | Stable ordering and pagination |
| Table or column choice | Avoid arbitrary selection; use approved mappings | Data model and least privilege |
| Saved filter definition | Treat as data under a defined schema | Do not execute stored text as SQL |
Parameters do not select arbitrary database structure
A value placeholder is not a general mechanism for inserting table names, column names, operators or complete ordering clauses. Those are part of the instruction structure. Use fixed queries or a reviewed mapping from a narrow product choice to known structure.
Be careful with query-builder escape hatches described as raw expressions. A query builder can bind ordinary filters correctly while a custom sorting fragment remains unsafe. Review the final construction and parameters at the driver boundary, keeping private values out of logs.
Do not broaden the structural allowlist merely because a request failed. Ask which options the product actually supports. Rejecting an unsupported sort key is useful validation, not a reason to accept arbitrary instructions.
Prepared statements have implementation-specific lifecycles
PostgreSQL documents parameter values and session-scoped prepared statements. That illustrates why syntax, caching and reuse belong to the actual database/driver contract. It does not justify replacing a supported framework query API with copied PostgreSQL commands in an unrelated stack.
Do not confuse a performance claim with a security guarantee. A prepared statement can have different planning behavior according to its environment. The security question here is separation of instructions and values; a faster query is not proof that separation exists.
For connection pools or serverless code, have the maintainer check the documented driver behavior. Avoid manually retaining a statement identifier across contexts where its lifetime is not supported. Handle driver errors without exposing database details to visitors.
Limit the consequences independently
Use a database identity with permissions appropriate to the application’s task. A reporting component should not receive broad administrative rights merely because the server can connect. Parameter binding does not reduce excessive database privilege or fix an unrelated access-control error.
Application authorization also remains necessary. A perfectly parameterized query can still return another member’s private record if the handler never checks ownership. The temporary-access guide explains the human role boundary; database and application checks need their own design.
A security plugin’s sales label cannot establish that custom queries are safe. The free versus paid security guide helps separate protective features from responsibilities. Maintain backups with an understood restore path through the restorable backup guide, while recognizing that backups do not prevent injection.
Ask for evidence at the query boundary
- Identify the actual code and driver that assemble the operation.
- Confirm values are bound through supported APIs after structure is defined.
- Review every structural choice and raw-query escape hatch.
- Check input rules, account permissions and database privilege separately.
- Run targeted authorized staging checks with synthetic data and inspect resulting records.
- Retain a reviewed diff and note any unexamined query paths.
Your next step is to review one custom query with its owner. Record the fixed structure, bound values and allowed structural choices without logging secrets. That concrete boundary is stronger evidence than a scanner result or an unsupported claim that the entire website is certified safe.
Sources and verification
Editorial review 2026-10-17. Source check dates are recorded separately. Notes describe what each source supports and its limits.
View sources
- OWASP: SQL injection preventionChecked Parameterized values, identifier allowlists and least database privilege.
Limits: Does not establish that a particular query builder or stored procedure is safe. - Python: sqlite3 placeholdersChecked DB-API binding of values through placeholders.
Limits: Python sqlite3 syntax is an illustration, not a portable database-driver contract. - PostgreSQL: PREPAREChecked Prepared-statement parameter values and session scope.
Limits: Database-specific syntax and performance behavior should not be generalized.