A search box becomes a SQL injection risk when its text is pasted into a query string: "SELECT * FROM products WHERE name = '" + searchTerm + "'". The database cannot tell which quote marks came from the application and which came from the user. Text meant to be a product name may change what the query does. The fix is to keep the SQL statement separate from the values supplied to it.
The same problem can start with login names, URL parameters, imported CSV fields, saved profile text, or values returned by another service. The source of the input is not what defines SQL injection. The problem is letting data become SQL syntax.
What SQL injection changes
An application sends the database a statement for a particular job: find an account, insert an order, or update a record. Concatenate untrusted text into that statement, and the resulting SQL may do something else. Depending on the query and the database account’s permissions, that could mean exposing records, changing or deleting data, or bypassing a condition the application was supposed to enforce.
Suppose an application looks up a customer ID. The unsafe approach puts the supplied ID directly into the SQL text. A safer approach sends a fixed statement such as SELECT email FROM customers WHERE customer_id = ? and supplies the ID separately. The driver binds it as a parameter, so quote marks or SQL-looking words in the value remain data, not operators.
This matters even for data that appears to come from inside the system. A suspicious value might be stored in a legitimate record, then retrieved for a report. If the reporting code concatenates it into another query, the unsafe boundary is still there. Review where each query is built, not only where a value first enters the application.

Use parameters for values, every time
Parameterized queries, also called prepared statements, are the default way to handle values in SQL. Write the statement with placeholders, then bind values through the database API. That applies to strings, numbers, dates, and LIKE searches. An integer-only form field is no reason to go back to concatenation.
A Java JDBC example
With JDBC, the statement stays fixed while the customer ID is bound:
String sql = "SELECT email FROM customers WHERE customer_id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, customerId);
try (ResultSet results = statement.executeQuery()) {
if (results.next()) {
String email = results.getString("email");
// Use the result only after checking application authorization.
}
}
}
The placeholder does not tell application code to perform string replacement. setLong passes a typed value to the driver, which handles database-specific binding. For text, use the appropriate setter, such as setString. In a code review, look for the opposite pattern: user-controlled text joined to SQL with +, interpolation, or a formatting function.
Binding does not replace authorization. A parameterized query can still return another customer’s record if the application fails to check access. If an order should be visible only to its owner, scope the request to the authenticated account rather than relying on an order number being hard to guess.
Where placeholders do not apply
Bind parameters represent values, not identifiers or SQL keywords. A placeholder generally cannot stand in for a table name, a column name, or ASC versus DESC. Binding a requested sort column as a value will either change the query’s meaning or cause an error; it will not create a safe column reference.
Instead, map accepted choices to SQL fragments written by the developer:
String orderBy = switch (sortKey) {
case "name" -> "name";
case "newest" -> "created_at";
default -> "product_id";
};
String direction = descending ? "DESC" : "ASC";
String sql = "SELECT product_id, name FROM products ORDER BY "
+ orderBy + " " + direction;
Neither appended fragment is raw request text; both come from fixed choices in the program. Use the same approach for selectable report columns or table names. If a feature truly needs arbitrary database identifiers, reconsider its design or use a database-specific identifier-quoting API with careful review. Ordinary value parameters cannot solve that problem.
Handle common query variations without concatenating input
A few common query patterns take extra planning. None require pasting user-supplied values into SQL.
- Partial matches: Put wildcards in the bound value, such as
statement.setString(1, "%" + term + "%"), and keepWHERE name LIKE ?fixed. If users need to search for literal percent or underscore characters, define an escape character and escape thoseLIKEwildcards according to your database’s rules. Wildcard handling controls search meaning; it is separate from injection prevention. - Variable-length lists: Build enough placeholders for a bounded list, such as
IN (?, ?, ?), and bind every element. Handle an empty list explicitly rather than generating invalid SQL. Some drivers and databases also support array or table-valued parameters. - Optional filters: Assemble the statement from fixed, developer-written clauses, then bind values for the filters present. Do not paste a submitted field name or comparison operator into a clause.
- Pagination: Bind the limit and offset where the driver supports it. Parse and cap those numbers too: a huge page size can waste resources even when it cannot change SQL syntax.
Validation still has a job. Require a positive customer ID, cap a page size, or parse a date as a real date. Those checks enforce application rules and limit misuse, but they do not replace binding. A restrictive form may later become a less restrictive API input; the SQL construction should remain safe either way.
Know what your data-access layer actually does
Object-relational mappers and query builders usually bind values when you use their intended APIs. They may also offer raw SQL methods, string-based query languages, or ways to build expressions. User text concatenated into a query remains risky even when an ORM sends that query to the database. Check the method you are using rather than assuming the framework makes every query safe.
Stored procedures need the same scrutiny. Calling a procedure with bound arguments can establish a clear boundary, but a procedure that concatenates an argument into SQL recreates the problem inside the database. Inspect statements built in procedure code, particularly in reporting and administrative procedures.
Escaping is a fragile primary defense. The right rule depends on the database, connection settings, character encoding, and where the text appears. It is easy to use the wrong rule or miss a code path. Bind values instead; reserve escaping for a specific context that genuinely needs it.

Limit the damage a database account can do
Parameters prevent values from changing a query’s structure. Database permissions limit what can happen if something else goes wrong. Give an application account only the permissions its normal work requires: a read-only reporting service, for example, should not be able to update orders. Do not use a database administrator account for routine application connections.
Where practical, use separate accounts for different application roles and grant access only to the needed tables or views. Handle schema changes during deployment through a separate, controlled account. Restrict sensitive tables even if current application code does not query them. Permissions will not fix concatenated SQL, but they can keep one vulnerable feature from reaching unrelated data.
Keep credentials out of source control and diagnostic output, and rotate them if exposed. Connection settings, access controls, and backups are useful safeguards. None make a concatenated query safe.
Test the boundary, not just the happy path
Tests should show that expected searches work and that unusual input stays a value. In an isolated development environment or one you are authorized to test, create a small fixture database and exercise each path that builds SQL: account lookups, filters, exports, update forms, and background jobs. Try quote marks, backslashes, percent signs, Unicode text, and empty strings where the field allows them. Check returned records and database state, not just whether a request produced an error.
For instance, insert a product named O'Brien's Notebook, search for that exact name, and verify that only the intended product comes back. It is ordinary data, but it can expose code that relies on accidental quoting rather than binding. When a review finds a concatenated query, add a regression test for the repaired path so later changes do not bring it back.
Static analysis can flag suspicious query construction, but review the findings. Search for SQL keywords near formatting or concatenation, then trace each appended fragment: is it a fixed constant, an allowed choice, or an untrusted value? SQL built in stored procedures and migration scripts may need separate inspection. Run these tests only on systems you own or are explicitly authorized to assess; do not use production data as a fixture.
Watch for misleading signals
A syntax error after a malformed search is a warning, not a diagnosis. It could be an ordinary bug, and a lack of errors does not prove a query is safe. A web application firewall may block some suspicious requests without fixing the code that builds the query. Use errors and alerts as reasons to inspect the statement and verify binding.
Do not show raw SQL errors to users. A public response can give a generic failure message while server-side logs retain a request identifier, the operation name, and enough context to investigate. Avoid logging full queries with sensitive bound values: a login name, personal record, or search term may be private. Use the request identifier to find the code path and check how values reached the statement.
A focused review checklist
For each database-backed feature, follow an input through to the executed statement:
- Identify every variable part of the statement, including sorting, filters, table selection, limits, and optional clauses.
- Bind all data values through the driver, ORM, or query builder’s parameter API.
- Choose necessary identifiers and SQL keywords from fixed, developer-controlled options.
- Validate types, lengths, and ranges for application behavior, without relying on validation to prevent injection.
- Check that the database account has only the permissions the operation needs.
- Test ordinary edge cases and repaired paths in an isolated environment.
Start by searching the codebase for SQL strings joined to variables. For each match, identify whether the variable is a bound value, a fixed fragment, or an allowed choice. If it is none of those, repair the query before building another feature on top of it.
