Under attack? Get incident response help

Web security

Find string-built SQL queries and replace them with parameters

How to find concatenated SQL in application code and stored procedures, replace it with bound parameters, and limit database privileges.

Also in:TürkçeDeutschFrançaisEspañolItalianoPolskiNederlandsEesti

Illustration: Find string-built SQL queries and replace them with parameters

Any SQL statement built by joining strings with untrusted values can be injected, and the fix is the same in every language: keep the SQL fixed and bind the values as parameters. The OWASP Query Parameterization Cheat Sheet files SQL injection under A05:2025-Injection in the OWASP Top 10:2025 and calls it one of the most dangerous web vulnerabilities.

Why concatenation fails

SQL makes no real distinction between the control plane and the data plane. An attacker places a meta character in an input, and SQL commands appear where none existed before. The OWASP community page on SQL injection shows a C# query built by concatenation:

string query = "SELECT * FROM items WHERE owner = '"
    + userName + "' AND itemname = '"
    + ItemName.Text + "'";

If the attacker enters name' OR 'a'='a as the item name, the condition always evaluates to true. The query returns every row in the items table, whatever its owner. Depending on database features and privileges, the same flaw can also modify data, affect files or run operating system commands.

The origin of the value does not matter. The controls apply whether the data comes from an HTTP request, another service or previously stored data.

Find the string-built queries

Start with a grep for SQL keywords sitting near concatenation or formatting. These patterns are a rough first pass, not a scanner. They are noisy, and they miss queries split across several lines.

# concatenation (Java, C#, JavaScript, PHP uses "." instead of "+")
grep -rnEi "(select|insert into|update|delete from)[^;]*[\"'] *\+" src/
# Python f-strings and .format()
grep -rnEi "f[\"'](select|insert|update|delete)|(select|insert|update|delete).*\.format\(" .
# JavaScript template literals
grep -rnEi '`(select|insert|update|delete)[^`]*\$\{' src/
# sprintf-style formatting
grep -rnEi "sprintf\(.*(select|insert|update|delete)" .

Read every hit and check that untrusted values are bound rather than concatenated. Then look in the places a grep tends to skip:

  • ORM query strings. Hibernate queries can be vulnerable when values are concatenated into the query string instead of bound.
  • Stored procedures. They keep SQL in the database but are not automatically safe. OWASP shows a PL/SQL procedure that joins usr and itm into a query string and says it is "vulnerable to the same SQL injection attack shown in the first example". Binding the arguments of the call protects the call, not dynamic SQL built inside the procedure.
  • Client-side parameterisation. Many client-side libraries build queries by string concatenation and send the raw query to the server. Confirm that parameterisation happens server-side.
  • Dynamic identifiers and sort directions. These cannot be bound, so they are where concatenation survives a parameterisation sweep.

Then test. The OWASP Web Security Testing Guide has SQL injection tests for manual and automated testing, including blind and out-of-band techniques. For an outside check on what your review missed, that is the job of a penetration test.

Replace them with parameters

Define the SQL structure separately from its data values, then bind the values through the database API. This is the example from the OWASP SQL Injection Prevention Cheat Sheet, with cleanup and error handling omitted:

String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, customerName);
ResultSet results = statement.executeQuery();

A value such as tom' or '1'='1 is now treated as the literal username to match. Concatenating an untrusted value into the query before preparing it defeats the protection.

With an ORM, bind in the query string too. The OWASP examples use a named parameter in HQL, FROM Inventory i WHERE i.productDescription = :productDescription, and pass the value with setParameter.

For dynamic SQL inside stored procedures, use bind variables. In Oracle PL/SQL that means EXECUTE IMMEDIATE with USING. In SQL Server it means sp_executesql with a declared parameter list. Procedures with no dynamic SQL need nothing special, because their parameters are already bound.

Binding stops a value from changing the SQL syntax. It does not decide whether the caller may access the matching account, so your authorisation checks stay in place.

What parameters cannot cover

Parameter markers stand for data values. They cannot stand for table names, column names or keywords such as ASC and DESC. When users must choose, map the selection to a small set of application-owned tokens and reject anything unknown. Before:

String query = "SELECT id, name FROM items ORDER BY name " + sortParam;

After:

Map<String, String> directions = Map.of("asc", "ASC", "desc", "DESC");
String direction = directions.get(sortParam);
if (direction == null) {
    throw new IllegalArgumentException("Unknown sort direction");
}
String query = "SELECT id, name FROM items ORDER BY name " + direction;

The text appended to the query is now always one of two literals, never what the user sent. Keep binding every data value in the same query, even when it passes type, length or format checks.

Do not fall back on manual escaping. It depends on the SQL dialect and configuration: MySQL's NO_BACKSLASH_ESCAPES mode, for example, disables backslash escaping. OWASP marks escaping as strongly discouraged and describes deny lists as riddled with loopholes.

Limit the damage

Least privilege does not prevent SQL injection. It limits what an attacker can do through a database account, so apply it alongside parameters.

  • Start from nothing and add the rights each application account needs. Working out what to take away from a broad grant is how rights get missed.
  • DBA or admin access for an application account is the usual shortcut, because everything "just works". OWASP calls it very dangerous.
  • A login page needs read access to the username and password fields and no write access. A sign-up page needs insert. That split is only enforceable if the two use different database users, so give each web application its own.
  • Attackers do not need injection to abuse an over-privileged account. They can change a parameter from a legal value to one they are not authorised for but the application is, which is why narrow grants help even where the queries are clean.
  • A view can expose only the fields an account needs, with SELECT granted on the view and not the table. Check your database's rules first: PostgreSQL normally checks underlying table access using the view owner's privileges. Queries against views still need parameters.
  • The operating system account matters as well. Do not run the DBMS as root or system. OWASP notes that MySQL runs as system on Windows by default.

Where to start

  1. List every place that builds SQL: application code, ORM query strings and stored procedures that use dynamic SQL. Run the grep patterns above as a first pass.
  2. Replace concatenated values with bound parameters, server-side, and rebuild dynamic SQL inside procedures with bind variables.
  3. Replace dynamic table, column and sort choices with fixed mappings that reject unknown input.
  4. Audit database accounts. Remove DBA and admin rights from application accounts, give each application its own user and move read-only access onto narrow grants or views.
  5. Run the Web Security Testing Guide's SQL injection tests against the result, or ask our secure software development team to review the changes.

How we can help

Penetration testing

We attack your networks, applications and people the way a real adversary would, then show you how to close each gap.

Contact us
AngelForce team

Written by the AngelForce team: security consultants with CEH, LPT Master and CISA certifications who test, monitor and defend systems for organisations across Europe.

Written with AI assistance from public sources listed below, and reviewed by the AngelForce team before publication.

Sources

  1. cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
  2. cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html
  3. community.owasp.org/attacks/SQL_Injection

Articles about Web security

Tell us what you need to protect

Describe your environment and what you want to achieve. We will reply by email.

Contact us
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonia