I am trying to implement PreparedStatement, which won't work with sql DB.
Suppose I have the following sql query:
String selectSqlQuery = "SELECT * FROM customer WHERE f1 = ? AND f2 =? AND f3 > ?";
and the following code:
//----
prest = con.prepareStatement(selectSqlQuery );
prest.setString(1, "val1");
prest.setString(2, "val2");
prest.setInt(3, 108);
ResultSet rs = prest.executeQuery();
//---
My question is how to implement setString and setInt methods for injecting params?
For now I save parameters' indexes and values into HashMap, but after it I can't make injection into sql query string.
implementation of sql's java interfaces are part of vendor specific jdbc driver. You probably just need to get the proper jdbc jar file for you database. writing implementations of such stuff is usually just needed if you intend to write your own database driver...
Since you're writing your own driver, you can play with your class a little. Let's change the approach. If you have a query like this one:
"SELECT * FROM table WHERE id = ? AND name = ?"
Replace the ? to turn it into
"SELECT * FROM table WHERE id = {0} AND name = {1}"
About your set methods, those will have to save your new parameters in an Object array, again matching against the index.
Object parameterArray = new Object[1];
public boolean setString(int paramIndex, String param) {
if(paramIndex < 0 || paramIndex > parameterArray.length)
throw new IllegalArgumentException("Can't set parameter " + paramIndex + ", The query only has " + parameterArray.length + " parameters.");
parameterArray[paramIndex - 1] = param;
}
Before executing the query, take advantage of your formatted string and set the parameters:
MessageFormat messageFormat = new MessageFormat(query);
String newQuery = messageFormat.format(parameterArray);
The format method will replace the {number} substrings for the corresponding element in the index represented by the number between brackets.
Related
I am having code something like this.
final PreparedStatement stmt = connection
.prepareStatement("delete from " + fullTableName
+ " where name= ?");
stmt.setString(1, addressName);
Calculation of fullTableName is something like:
public String getFullTableName(final String table) {
if (this.schemaDB != null) {
return this.schemaDB + "." + table;
}
return table;
}
Here schemaDB is the name of the environment(which can be changed over time) and table is the table name(which will be fixed).
Value for schemaDB is coming from an XML file which makes the query vulnerable to SQL injection.
Query: I am not sure how the table name can be used as a prepared statement(like the name used in this example), which is the 100% security measure against SQL injection.
Could anyone please suggest me, what could be the possible approach to deal with this?
Note: We can be migrated to DB2 in future so the solution should compatible with both Oracle and DB2(and if possible database independent).
JDBC, sort of unfortunately, does not allow you to make the table name a bound variable inside statements. (It has its reasons for this).
So you can not write, or achieve this kind of functionnality :
connection.prepareStatement("SELECT * FROM ? where id=?", "TUSERS", 123);
And have TUSER be bound to the table name of the statement.
Therefore, your only safe way forward is to validate the user input. The safest way, though, is not to validate it and allow user-input go through the DB, because from a security point of view, you can always count on a user being smarter than your validation.
Never trust a dynamic, user generated String, concatenated inside your statement.
So what is a safe validation pattern ?
Pattern 1 : prebuild safe queries
1) Create all your valid statements once and for all, in code.
Map<String, String> statementByTableName = new HashMap<>();
statementByTableName.put("table_1", "DELETE FROM table_1 where name= ?");
statementByTableName.put("table_2", "DELETE FROM table_2 where name= ?");
If need be, this creation itself can be made dynamic, with a select * from ALL_TABLES; statement. ALL_TABLES will return all the tables your SQL user has access to, and you can also get the table name, and schema name from this.
2) Select the statement inside the map
String unsafeUserContent = ...
String safeStatement = statementByTableName.get(usafeUserContent);
conn.prepareStatement(safeStatement, name);
See how the unsafeUserContent variable never reaches the DB.
3) Make some kind of policy, or unit test, that checks that all you statementByTableName are valid against your schemas for future evolutions of it, and that no table is missing.
Pattern 2 : double check
You can 1) validate that the user input is indeed a table name, using an injection free query (I'm typing pseudo sql code here, you'd have to adapt it to make it work cause I have no Oracle instance to actually check it works) :
select * FROM
(select schema_name || '.' || table_name as fullName FROM all_tables)
WHERE fullName = ?
And bind your fullName as a prepared statement variable here. If you have a result, then it is a valid table name. Then you can use this result to build a safe query.
Pattern 3
It's sort of a mix between 1 and 2.
You create a table that is named, e.g., "TABLES_ALLOWED_FOR_DELETION", and you statically populate it with all tables that are fit for deletion.
Then you make your validation step be
conn.prepareStatement(SELECT safe_table_name FROM TABLES_ALLOWED_FOR_DELETION WHERE table_name = ?", unsafeDynamicString);
If this has a result, then you execute the safe_table_name. For extra safety, this table should not be writable by the standard application user.
I somehow feel the first pattern is better.
You can avoid attack by checking your table name using regular expression:
if (fullTableName.matches("[_a-zA-Z0-9\\.]+")) {
final PreparedStatement stmt = connection
.prepareStatement("delete from " + fullTableName
+ " where name= ?");
stmt.setString(1, addressName);
}
It's impossible to inject SQL using such a restricted set of characters.
Also, we can escape any quotes from table name, and safely add it to our query:
fullTableName = StringEscapeUtils.escapeSql(fullTableName);
final PreparedStatement stmt = connection
.prepareStatement("delete from " + fullTableName
+ " where name= ?");
stmt.setString(1, addressName);
StringEscapeUtils comes with Apache's commons-lang library.
I think that the best approach is to create a set of possible table names and check for existance in this set before creating query.
Set<String> validTables=.... // prepare this set yourself
if(validTables.contains(fullTableName))
{
final PreparedStatement stmt = connection
.prepareStatement("delete from " + fullTableName
+ " where name= ?");
//and so on
}else{
// ooooh you nasty haker!
}
create table MYTAB(n number);
insert into MYTAB values(10);
commit;
select * from mytab;
N
10
create table TABS2DEL(tname varchar2(32));
insert into TABS2DEL values('MYTAB');
commit;
select * from TABS2DEL;
TNAME
MYTAB
create or replace procedure deltab(v in varchar2)
is
LvSQL varchar2(32767);
LvChk number;
begin
LvChk := 0;
begin
select count(1)
into LvChk
from TABS2DEL
where tname = v;
if LvChk = 0 then
raise_application_error(-20001, 'Input table name '||v||' is not a valid table name');
end if;
exception when others
then raise;
end;
LvSQL := 'delete from '||v||' where n = 10';
execute immediate LvSQL;
commit;
end deltab;
begin
deltab('MYTAB');
end;
select * from mytab;
no rows found
begin
deltab('InvalidTableName');
end;
ORA-20001: Input table name InvalidTableName is not a valid table name ORA-06512: at "SQL_PHOYNSAMOMWLFRCCFWUMTBQWC.DELTAB", line 21
ORA-06512: at "SQL_PHOYNSAMOMWLFRCCFWUMTBQWC.DELTAB", line 16
ORA-06512: at line 2
ORA-06512: at "SYS.DBMS_SQL", line 1721
I'm working on a small program that lists local train stops in a numbered list then asks for the user to type the number of the station that they wish to see the next arrival time for.
The problem I have is I don't think the MySQL query is correct to retrieve the arrival time. The list returns empty. Using jdbc previously, this query worked fine:
"SELECT arrival_time FROM stop_times WHERE stop_id = '"
+ myStation.getID()
+ "' AND arrival_time > time('now', 'localtime') ORDER BY arrival_time asc;";
And the current hibernate query:
public List<String> getArrivals() {
sessionFactoryBean.getCurrentSession().beginTransaction();
String sql = "SELECT arrival_time FROM stop_times WHERE stop_id = '"
+ myStation.getID()
+ "' AND arrival_time > time('now', 'localtime') ORDER BY arrival_time asc;";
Query query = sessionFactoryBean.getCurrentSession()
.createSQLQuery(sql)
.addEntity(Station.class);
List<String> arrivals = query.list();
sessionFactoryBean.getCurrentSession().getTransaction().commit();
return arrivals;
}
Called from this method and where I get IndexOutOfBoundsException:
public String getNextArrival(int user_input) {
getStationName(user_input);
List<String> arrivals1 = arrival.getArrivals();
System.out.println(arrivals1);
System.out.println(arrivals1.size());
String arrivalTime = arrivals1.get(user_input);
return convertTime(arrivalTime);
}
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0,
Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at com.moeller.code.Stops.getNextArrival(Stops.java:73)
Line 73 String arrivalTime = arrivals1.get(user_input);'
The DataBase is stored locally.
There are several problems with this.
First, the query does not use parameters, which means you will likely make this mistake elsewhere where it will be a danger. You have to pass on variables like this:
String sql = "SELECT arrival_time FROM stop_times WHERE stop_id = ?"
+ " AND arrival_time > time('now', 'localtime') ORDER BY arrival_time asc;";
Query query = sessionFactoryBean.getCurrentSession()
.createSQLQuery(sql)
.addEntity(Station.class);
query.setParameter(1, myStation.getID());
See the question mark? That is a positional parameter. You can also use named parameters.
String sql = "SELECT thing FROM table WHERE column1 LIKE :ptrn";
...
query.setParameter("ptrn", "%that%");
Notice how inside the query the parameter starts with :, but it does not when calling setParameter.
This way of safely inserting parameters is called using "Prepared Statements", or "Parameterized Queries". Find a quick tutorial on them, they are very important.
Secondly, in getNextArrival you forget to check if the list has that many elements.
if (arrivals1.size() <= user_input) {
return null;
}
Of course then you have to be careful when it returns a null to the function where it's used.
You are using a wrong method for the List.
when yo use List.get(param) param should be the position that you are looking for, no the userInput.
you need loop the list and compare each position of the list with the user input.
best Regards
I have the following section of code to implement a drop-down menu. Based on the two parameters selected from the menu, I run the corresponding query by replacing the selected column with the mapped column name.
String sql = "SELECT ?, ? FROM <table_name>";
ArrayList<Some_POJO> res = this.jdbcTemplate.query(sql, new ResultSetExtractor<Some_POJO>()
{//logic goes here.},
user_inputs_as_object_array);
Now, in the logic section, I'm using the following command to map the returned values:
while(rs.next()) {
Some_POJO = new SOME_POJO();
Some_POJO.setParam1(rs.getString("SOME_COLUMN_NAME")); //ERROR
}
Now, the marked line fails when I refer to column by name. But what is surprising is, the following workaround works:
while(rs.next()) {
Some_POJO = new SOME_POJO();
int i = 1;
Some_POJO.setParam1(rs.getString(i)); //Works(?!)
}
I tried to modify the JDBC template call to return the ResultSetMetaData object instead of an ArrayList of Some_POJO:
String sql = "SELECT ?, ? FROM <table_name>";
ResultSetMetaData res = this.jdbcTemplate.query(sql, new ResultSetExtractor<ResultSetMetaData>()
{//extractData now returns rs.getMetaData()},
user_inputs_as_object_array);
try
{
System.out.println("Column cout: " + res.getColumnCount());
for(int i = 1; i <= res.getColumnCount(); i++)
System.out.println("Label: " + res.getColumnLabel(i) + "\nName: " + res.getColumnName(i));
}
catch(SQLException sqle)
Only to get:
Column cout: 2
Label: :1
Name:
Label: :2
Name:
Could someone please shed some light into what is happening and (except for suggestions suggesting a complete redesign) what would be the most optimal way to achieve the expected functionality? I would prefer not to use column indexes since order changes would break the functionality.
Just for refactoring code, get values as:
ArrayList result = new ArrayList(0);
result.addAll(this.jdbcTemplate.query(sql, (RowMapper) (rs, rowNum) -> {
POJO out = new POJO();
out.setProperty1(rs.getString("prop1"));
out.setProperty2(rs.getString("prop2"));
...
return out;
}));
For me it works fine, passing field name in method, don't think that there are some troubles with spring jdbc. Maybe is something wrong in your sql query.
I have a query like this..
String query = "UPDATE tbl_customer_policies SET "+
"start_date = ?," +
"next_pay_date = ?,"+
"maturity_date = ?, " +
"modified_at = CURRENT_TIMESTAMP,"+
"modifier = ?, status = ? " +
"WHERE id = ?";
Now in place of the place-holder for start_date I want to pass a string like SYSDATE.
What I am doing now is setting that string in a variable called String startDate = "SYSDATE" and binding it to that place-holder. But I guess it does not seem to work. I get this exception
ORA-01858: a non-numeric character was found where a numeric was expected
I have to pass Oracle functions like that. How to achieve that?
If it will always be sysdate you don't need to parameterize it.
Set it directly in the query like this:
String query = "UPDATE tbl_customer_policies SET "+
"start_date = sysdate," + // or "start_date = ADD_MONTHS(SYSDATE, 12),"
"next_pay_date = ?,"+
"maturity_date = ?, " +
"modified_at = CURRENT_TIMESTAMP,"+
"modifier = ?, status = ? " +
"WHERE id = ?";
And set all the others parameters like you did before.
Use function TO_DATE (docs)
for example
"UPDATE tbl_customer_policies SET "+
"start_date = TO_DATE(?, 'DD-MON-RR')," +
"next_pay_date = TO_DATE(?, 'DD-MON-RR'),"+
and don't forget pass parameters in format like '27-OCT-98' in this case.
P.S.
I misunderstood the question so try to correct my answer.
The described problem is for limitations of PreparedStatement Java class.
PreparedStatement object is used for storing a reference to some precompiled (and optimized) SQL statement.
So you have to supply only values of parameters for filling them.
Functions must be evaluated before using their results and they aren't able to be placed there.
I guess you can use a workaround with Oracle Structured Types.
You can pass them as reference types with PreparedStatement's setRef() method.
Using functions can be implemented with a wrapper in constructor of object.
I didn't try it but it seems to me it is a possible solution.
I'm having an sql problem. I writing a java application on top of a Access database.
It's a search query for several fields and I know the error is in the part where I need to calculate the age of a person when he or she went missing. I'm returning a tablemodel in my method so i need to do the calculations in my query. My latest atempt to make it work is this:
public TableModel UpdateTable(String dossiernr, String naam, String voornaam,
String startleeftijd, String eindleeftijd, String dossierjaar, String geslacht)
{
TableModel tb = null;
String sql= "SELECT [Nr dossier],[Annee],[Nom],[Prenom],[Disparu le],[Ne le],[Sexe], DATEDIFF('yyyy',[Ne le],[Disparu le]) - iif(DATEADD('yyyy', DATEDIFF('yyyy',[Ne le],[Disparu le]),"
+ "[Ne le])>[Disparu le],1,0) AS Age FROM TotalTable "
+ "WHERE [Nr dossier] LIKE ? AND [Nom] LIKE ? AND [Prenom] LIKE ? AND [Annee] LIKE ? AND Age >= ? AND Age <= ? AND [Sexe] LIKE ?;";
try
{
PreparedStatement pstatement;
Connection connection = PersistentieController.getInstance().getConnection();
pstatement = initStatement(connection,sql);
pstatement.setString(1, "%" + dossiernr + "%");
pstatement.setString(2, "%" + naam + "%");
pstatement.setString(3, "%" + voornaam + "%");
pstatement.setString(4, "%" + dossierjaar + "%");
pstatement.setString(5, startleeftijd);
pstatement.setString(6, eindleeftijd);
pstatement.setString(7, "%" + geslacht + "%");
rs=pstatement.executeQuery();
tb = DbUtils.resultSetToTableModel(rs);
pstatement.close();
}//einde try
catch (SQLException e)
{
e.printStackTrace();
} //einde catch
return tb;
}
When i run it, i get following error:
java.sql.SQLException: [Microsoft][ODBC Microsoft Access Driver] Too few parameters. Expected 8.
I only work with 7 parameters and don't get why he's asking for 8.
Thanks
You count 7 parameters in your WHERE clause. Unfortunately, the Access db engine treats Age as another parameter in that situation, so it thinks you have 8 parameters instead of only 7.
To understand why, start with this query which runs without error with my Access database:
SELECT some_text AS foo
FROM tblFoo
WHERE some_text Is Not Null;
However, when attempting to use the alias instead of the field name in the WHERE clause, Access prompts me to supply a value for foo because it treats it as a parameter:
SELECT some_text AS foo
FROM tblFoo
WHERE foo Is Not Null;
Access limits your ability to re-use alias names later in a query. In certain cases, it will accept the alias, but yours is not one of those cases.
You could define the alias in a subquery. Then the db engine will recognize it correctly when you reference the subquery's alias in the parent query.
If possible, test your SQL statements directly in Access. If they fail, that effort will give you the best chance to determine why.