3

Our application has a filtering capability, where the database query is built dynamically as per the user-entered filter values. Prepared Statements are not an option for us.

All the filters are text filters, so we have the luxury to use IN keyword instead of the equals (=) operator.

We're currently checking the possibility of SQL injections that can be caused by the user text inputs.

I think since we're not using the = operator and we wrap the user input by single quotes, we can negate the possibility of SQL injection by handling the single quote (') gracefully.

StringBuilder resultBuilder = new StringBuilder(""); resultBuilder.append("'").append(input.trim()).append("'"); 

We're thinking of restricting single quotes for user inputs. Is it possible to break the query in our scenario, if we restrict single quote?

Etc. If we do not handle the single quote, query can be injected like this:

SELECT ItemName, ItemDescription FROM Items WHERE ItemNumber IN ('userInput'); 

If user enters something like, 'Alice'); DROP TABLE users;-- the query becomes,

SELECT ItemName, ItemDescription FROM Items WHERE ItemNumber IN ('Alice'); DROP TABLE users;--'); 

PS: We have a capability where the user can select any database table to a GUI grid to display data, and add custom filters according to that selected table, to filter that grid data. So the DB entities are not predefined at the code. This makes it impossible or messy to use Prepare statements.

  • DB Server: Postgres
  • Language: Java
  • Framework: Spring boot, JdbcTemplate
17
  • 65
    Please explain why you think you can't use a prepared statement? Commented Dec 16, 2024 at 15:59
  • 8
    @AkiT: JdbcTemplate works just fine with prepared statements. For example, you can pass a PreparedStatementGetter as a second parameter to some variants of the JdbcTemplate#query method. I understand that you need to build the query dynamically based on the user-provided filters, so it's all a bit inconvenient. But it's certainly doable. Commented Dec 17, 2024 at 9:42
  • 15
    "It makes impossible or messy to use Prepare statements." - not messier than working with strings Commented Dec 17, 2024 at 9:46
  • 25
    In my career as a software developer, every time I have come across a developer claim that X is impossible to do, they have invariably been completely wrong. Commented Dec 17, 2024 at 10:39
  • 8
    @AkiT I still see no reason why you cannot use prepared statements even if you build the query dynamically. Commented Dec 17, 2024 at 14:40

6 Answers 6

39

Naive escaping or filtering that doesn't take the character encoding of the database connection into account is dangerous, because your code and the database system may not agree on what a single quote actually looks like as a byte sequences. Under specific circumstances, an attacker can exploit this to bypass the escaping or filtering altogether. This is why, for example, MySQL has replaced the library function mysql_escape_string (which doesn't have a parameter to specify the encoding) with mysql_real_escape_string (which does).

In general, don't try to invent your own filters. Use well-tested standard methods provided by the database library. First, double-check if you really cannot use prepared statements. I find that hard to believe. If you're sure that there's no way to fix this major limitation, then use the escape functions of the database library, not your own.

1
22

There is absolutely no reason why you can't use parameterised statements and let your DB framework handle the rest. This is going to be a lot safer than trying to escape things with whatever methods you could come up with.

Remember that PostgreSQL knows about arrays, and allows you to match to any value in an array:

SELECT * FROM thing WHERE stuff = ANY(:list) 

You then pass your list of values as an array, and this will be equivalent to IN.

13
  • 9
    If both the target database and the driver / DBAL in use support it, an array parameter would indeed be perfect. Where that combination is not possible, the common approach is to generate a statement with an appropriate number of placeholders (?, ?, ? etc) Commented Dec 17, 2024 at 12:34
  • 1
    @IMSoP if arrays aren't supported, you could still use something like value in string_split(:list, ';') and pass the list as a semicolon separated string. Commented Dec 17, 2024 at 18:26
  • 1
    @Berend that would still require the DB to support some kind of array or list construct (to even be able to have such a function). For instance, I don't think MySQL has such a function (but many MySQL drivers support expanding the list client-side) Commented Dec 17, 2024 at 18:33
  • I take your word for it :-) At least sqlserver, oracle and postgresql have similar string split functions. Commented Dec 17, 2024 at 19:07
  • 1
    @Jasen but at least thing and stuff are from a finite list, and can easily be validated Commented Dec 18, 2024 at 6:32
11

YES.


If you create an SQL query using a user string without using appropriate PPE (IE prepared statements). Your code is subject to SQL injection attacks.

This question starts with a false assumption:


"Prepare Statements are not an option for us.". Not only are they always an option (unless this is a homework assignment). They are the best/easiest/most safe way to do things.

Java with PostgreSQL supports prepared statements. You can either dynamically generate your prepared statement (not using untrustworthy user strings.) And then prepare it and bind the evil user strings.

Or you can use an array and use the postgreSQL driver to convert a java array into a postgreSQL array.

Not using an array


This works, but not my favored way

You can generated a prepared statement with N number of tags. Following is some Pseudo Java.

String my_sql_string = "select something, other_thing, last_thing from my_table where X in (?"; // This is fine, string is generated by me, not using untrustworthy user input.... for (int n = 1; n < my_filters.length(); n++) { my_sql_string += ", ?"; } my_sql_string += ")"; PreparedStatement my_statement = dbConnection.preapareStatement(my_sql_string ); for (int n = 0; n < my_filter_options.length(); n++) { my_statement .setString(n+1, my_filters[n]); // This is 1 indexed due to reasons... :( } response = preparedStatemebnt.query(). //Do whatever you want with the response... 

With an array


PostgreSQL supports arrays.

String my_sql_string = "select something, other_thing, last_thing from my_table where x = ANY(?)"; PreparedStatement my_statement = dbConnection.preapareStatement(my_sql_string ); Array my_filters = connection.createArray("varchar", my_filters); my_statement.setArray(1, my_filters); // DO your thing 

Disclaimer

This is untested pseudo code, will probably not work as is, and I might have some method names wrong. But it will work with a small amount of tweaking.

8
  • Prepare Statements are not an option for us. It's my question and it stands for itself. If we wanted to use Prepare statements, I wouldn't post this question. Commented Dec 18, 2024 at 8:12
  • 10
    @AkiT Prepared statements are always an option. Build them dynamically if needed, as in this answer. Commented Dec 18, 2024 at 9:59
  • 11
    @AkiT If you don't want SQL injection vulnerabilities, then you want to use prepared statements. It's honestly that simple. Commented Dec 18, 2024 at 11:36
  • 1
    @AkiT Your language supports them. Your database supports them. It is extremely easy to generate them dynamically. The only, only reason why I can see someone not putting in prepared statements is that they want a flippin' hole in their codes security. Commented Dec 18, 2024 at 17:32
  • 2
    @Joshua That sounds like a bug that needs to be fixed with the database you were using. Commented Dec 19, 2024 at 19:03
3

We're thinking of restricting single quotes for user inputs. Is it possible to break the query in our scenario, if we restrict single quote?

Were thinking of messing with people's surnames.

double them instead.

If your database sessions has standard_conforming_strings turned off you'll also need to double backslashes.

If users get to specify column,table, database, or schema names you'll also need to deal with the identifier quoting rules.

You're connecting to arbitrary databases, you had better not be doing that as a database superuser else someone might slip in an ON SELECT TO tablename DO INSTEAD rule that invokes

create temp table foo(); copy foo from program 'rm -rf ../..'; -- this will nuke your database cluster 

or something else similarly dangerous.

1
  • This actually raises a good point, if you are going to implement a solution that might be susceptible to injection attacks, restrict the user account that is executing the queries to only allow read operations and/or to prevent execution of DDL operations. Commented Dec 22, 2024 at 11:55
1

You need to escape any user inputs. You should use a proper library to escape any user input and not try any homebrew methods. I'm not sure if jdbc has some base function to do it but perhaps there is a string lib available that will.

Prepared statements are useful for reusing a query with different params such as in a loop, using them for one off situation may not be most efficient depending on use case. https://www.postgresql.org/docs/current/sql-prepare.html

17
  • 8
    The primary benefit of prepared statements is that they reliabily prevent SQL injections, because they completely separate the parameter values (which may be user-provided) from the query structure itself. In this context, it’s irrelevant it the statement is reused or not. Escaping is inferior to prepared statements, because developers (and sometimes libraries) are notoriously bad at using it consistently and correctly, and because it relies on the character encoding (as I’ve explained in my answer). Commented Dec 17, 2024 at 8:54
  • The character encoding issue you mention is more relevant to mysql, this user is using postgresql. I am not disagreeing with the use of prepared statements for security, but that is not its primary purpose. Commented Dec 17, 2024 at 14:48
  • 2
    @jonk Preventing SQL injections is by far the most important application of prepared statements. Of course they can also be reused and may improve the performance, but this only applies to specific cases and has to be evaluated. If there’s a loop which executes a large number of individual queries, this can also be a sign of poor programming. For example, a join or a bulk insert may be more appropriate (depending on the exact context). Commented Dec 17, 2024 at 15:37
  • 1
    Also, It is easy to use a preparedStatement in this, and any situation. It is possible, and safe to dynamically generate prepared statements as long as evil user strings are not used during dynamic generation (Can count the number of evil user strings, as in the case of say a search filter etc...). Commented Dec 17, 2024 at 20:40
  • 1
    @jonk: You don't understand the attack. The SET NAMES is what the developer does, not the attacker. And the input is escaped -- where exactly it comes from doesn't matter. I could have used $_GET, $_POST etc. The reason for the 1 OR is that I wanted to display the first row in any case. This is getting a bit ridiculous. If you want to change the demo to something you like better, then do it yourself. If you want to ignore the well-known issues of escaping and simply declare everything you don't personally use (like MySQL?) as “unrealistic”, this discussion is pointless. Commented Dec 20, 2024 at 4:21
1

Specific database wasn't mentioned, however, as I work primarily in Oracle, I thought as a potential alternative option, this alternative approach might be worth considering:

Pros:

  • uses bind variables - so no sql injection.

Cons:

  • Sql statement is slightly more complex
  • no promises on performance as it scales, or gets more complex (that said, I've used similar trick and haven't yet hit any real issues .. but of course, YMMV)

Simple example to demonstrate in sqlplus. (note the begin block is just setting up the bind variable. Just because that's text, that isn't intended as your input .. it's just a substitute for a proper bind variable. The bind variable you send into this is a single String (comma delimited) with values ... and if extra "junk" or attempts to break it are added - they are simply ignored - unless the text itself actually matches the lookup ;) )

create table my_table ( id number primary key, col_val varchar2(10) ); insert into my_table values ( 123, 'Alice' ); insert into my_table values ( 143, 'Bob' ); insert into my_table values ( 223, 'Chuck' ); insert into my_table values ( 523, 'Daniel' ); insert into my_table values ( 753, 'Eunice' ); insert into my_table values ( 993, 'Fran' ); commit; select * from my_table; var b varchar2(1000) begin :b := 'Daniel, Alice, Drop Table, other junk, Fran'; end; / select :b from dual; :B ------------------------------------------------------------------------------------------------------------------------ Daniel, Alice, Drop Table, other junk, Fran with temp as ( select :b s from dual ), w_list as ( select level , trim(regexp_substr(t.s, '[^,]+', 1, level)) fs from temp t connect by level <= length ( regexp_replace(t.s, '[^,]+')) + 1 ) select * from my_table mt where mt.col_val in ( select fs from w_list ) / ID COL_VAL ---------- ---------- 523 Daniel 123 Alice 993 Fran 3 rows selected. 

Basically, the with clauses are taking the incoming comma delimited string, and parsing it out into individual rows. so "w_list" ends up looking/acting just like a table. So now you can join to it with normal sql statements. (in this case an IN clause to potentially avoid any duplicates)

The real point here is, there should never be a good reason NOT to use bind variables .. and there is likely ALWAYS a way to use them to avoid sql injection. It might require a redesign of your app or such, or just some creative thinking on how you pass in the values ... but there will be a way.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.