From c370b60c6b660079e35cf97a975033cef5c09b30 Mon Sep 17 00:00:00 2001 From: brinn <brinn> Date: Thu, 15 Nov 2012 13:27:38 +0000 Subject: [PATCH] [BIS-260] Fix SQL injections for custom queries. Add support for backward compatible specifications of strings with '${code}' and '{${array}}'. Add support for simplified specification of PostgreSQL arrays with "=ANY({${myarray}})". Add support for overriding the parameter type as placeholder metadata like in: ${code::varchar}. This is necessary to get things working for Oracle as the Oracle JDBC driver does not support calling ParameterMetaData.getParameterType(). SVN: 27635 --- .../cisd/openbis/plugin/query/server/DAO.java | 152 +++++++++++++++++- .../openbis/plugin/query/server/DAOTest.java | 52 +++++- 2 files changed, 197 insertions(+), 7 deletions(-) diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/query/server/DAO.java b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/query/server/DAO.java index cd0884505bc..46248c8c5a4 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/plugin/query/server/DAO.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/plugin/query/server/DAO.java @@ -30,12 +30,15 @@ import java.sql.Types; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import javax.sql.DataSource; +import org.apache.commons.lang.StringUtils; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCallback; @@ -45,6 +48,7 @@ import org.springframework.jdbc.support.JdbcUtils; import ch.systemsx.cisd.common.exceptions.UserFailureException; import ch.systemsx.cisd.common.string.Template; +import ch.systemsx.cisd.common.string.Template.IToken; import ch.systemsx.cisd.common.utilities.Counters; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DateTableCell; import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DoubleTableCell; @@ -72,6 +76,58 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO private static final String ENTITY_COLUMN_NAME_SUFFIX = "_KEY"; + private static final Map<String, Integer> SQL_TYPE_CODE_TO_TYPE_MAP = + new HashMap<String, Integer>(); + + private static final Map<Integer, String> SQL_TYPE_TO_TYPE_CODE_MAP = + new HashMap<Integer, String>(); + + static + { + SQL_TYPE_CODE_TO_TYPE_MAP.put("ARRAY", Types.ARRAY); + SQL_TYPE_CODE_TO_TYPE_MAP.put("BIGINT", Types.BIGINT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("BINARY", Types.BINARY); + SQL_TYPE_CODE_TO_TYPE_MAP.put("BIT", Types.BIT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("BLOB", Types.BLOB); + SQL_TYPE_CODE_TO_TYPE_MAP.put("BOOLEAN", Types.BOOLEAN); + SQL_TYPE_CODE_TO_TYPE_MAP.put("CHAR", Types.CHAR); + SQL_TYPE_CODE_TO_TYPE_MAP.put("CLOB", Types.CLOB); + SQL_TYPE_CODE_TO_TYPE_MAP.put("DATALINK", Types.DATALINK); + SQL_TYPE_CODE_TO_TYPE_MAP.put("DATE", Types.DATE); + SQL_TYPE_CODE_TO_TYPE_MAP.put("DECIMAL", Types.DECIMAL); + SQL_TYPE_CODE_TO_TYPE_MAP.put("DISTINCT", Types.DISTINCT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("DOUBLE", Types.DOUBLE); + SQL_TYPE_CODE_TO_TYPE_MAP.put("FLOAT", Types.FLOAT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("INTEGER", Types.INTEGER); + SQL_TYPE_CODE_TO_TYPE_MAP.put("JAVA_OBJECT", Types.JAVA_OBJECT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("LONGNVARCHAR", Types.LONGNVARCHAR); + SQL_TYPE_CODE_TO_TYPE_MAP.put("LONGVARBINARY", Types.LONGVARBINARY); + SQL_TYPE_CODE_TO_TYPE_MAP.put("LONGVARCHAR", Types.LONGVARCHAR); + SQL_TYPE_CODE_TO_TYPE_MAP.put("NCHAR", Types.NCHAR); + SQL_TYPE_CODE_TO_TYPE_MAP.put("NCLOB", Types.NCLOB); + SQL_TYPE_CODE_TO_TYPE_MAP.put("NULL", Types.NULL); + SQL_TYPE_CODE_TO_TYPE_MAP.put("NUMERIC", Types.NUMERIC); + SQL_TYPE_CODE_TO_TYPE_MAP.put("NVARCHAR", Types.NVARCHAR); + SQL_TYPE_CODE_TO_TYPE_MAP.put("OTHER", Types.OTHER); + SQL_TYPE_CODE_TO_TYPE_MAP.put("REAL", Types.REAL); + SQL_TYPE_CODE_TO_TYPE_MAP.put("REF", Types.REF); + SQL_TYPE_CODE_TO_TYPE_MAP.put("ROWID", Types.ROWID); + SQL_TYPE_CODE_TO_TYPE_MAP.put("SMALLINT", Types.SMALLINT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("SQLXML", Types.SQLXML); + SQL_TYPE_CODE_TO_TYPE_MAP.put("STRUCT", Types.STRUCT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("TIME", Types.TIME); + SQL_TYPE_CODE_TO_TYPE_MAP.put("TIMESTAMP", Types.TIMESTAMP); + SQL_TYPE_CODE_TO_TYPE_MAP.put("TINYINT", Types.TINYINT); + SQL_TYPE_CODE_TO_TYPE_MAP.put("VARBINARY", Types.VARBINARY); + SQL_TYPE_CODE_TO_TYPE_MAP.put("VARCHAR", Types.VARCHAR); + for (Map.Entry<String, Integer> entry : SQL_TYPE_CODE_TO_TYPE_MAP.entrySet()) + { + SQL_TYPE_TO_TYPE_CODE_MAP.put(entry.getValue(), entry.getKey()); + } + // Convenience mapping + SQL_TYPE_CODE_TO_TYPE_MAP.put("STRING", Types.VARCHAR); + } + private static Map<String, EntityKind> entityKindByColumnName = new HashMap<String, EntityKind>(); @@ -206,6 +262,59 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO return (TableModel) template.execute(resolvedQuery, callback); } + /** + * A class for figuring out the parameter type. + * <p> + * Note that some JDBC drivers (like the one from Oracle) have + * {@link ParameterMetaData#getParameterType(int)} not implemented and thus requires setting it + * explicitly in the variable metadata. + */ + private static class ParameterTypeProvider + { + private final Template template; + + private final Map<Integer, Entry<String, String>> indexMap; + + private final ParameterMetaData paramMD; + + ParameterTypeProvider(Template template, Map<Integer, Entry<String, String>> indexMap, + ParameterMetaData paramMD) + { + this.template = template; + this.indexMap = indexMap; + this.paramMD = paramMD; + } + + int getParameterCount() throws SQLException + { + return paramMD.getParameterCount(); + } + + int getParameterType(int param) throws SQLException + { + final Entry<String, String> entry = indexMap.get(param - 1); + final String overrideTypeCode = + StringUtils.upperCase(template.tryGetMetadata(entry.getKey())); + if (overrideTypeCode != null) + { + final Integer paramType = SQL_TYPE_CODE_TO_TYPE_MAP.get(overrideTypeCode); + if (paramType == null) + { + throw new SQLDataException("Invalid SQL type code '" + overrideTypeCode + "'"); + } + return paramType; + } else + { + return paramMD.getParameterType(param); + } + } + + String getParameterTypeName(int param) throws SQLException + { + return SQL_TYPE_TO_TYPE_CODE_MAP.get(getParameterType(param)); + } + } + private static PreparedStatementCreator createSQLPreparedStatement(final String sqlQuery, final QueryParameterBindings bindingsOrNull) { @@ -218,6 +327,22 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO final Map<Integer, Entry<String, String>> indexMap = new HashMap<Integer, Entry<String, String>>(); final Template template = new Template(sqlQuery); + final Set<String> arrayVariableNames = new HashSet<String>(); + // Support for legacy PostgreSQL array specifications + addToSet(template.replaceBrackets("'{", "}'", "", ""), arrayVariableNames); + // Support for legacy string parameters: replace '${var}' with ${var}. + template.replaceBrackets("'", "'", "", ""); + // Support for simplified array specification in PostgreSQL + addToSet(template.replaceBrackets("{", "}", "", ""), arrayVariableNames); + for (String arrayVariableName : arrayVariableNames) + { + final IToken token = template.tryGetRightNeighbor(arrayVariableName); + if (token != null && token.tryGetValue() != null + && token.tryGetValue().startsWith("::text[]") == false) + { + token.setSubString(0, 0, "::text[]", ""); + } + } if (bindingsOrNull != null) { for (Entry<String, String> entry : bindingsOrNull.getBindings().entrySet()) @@ -231,8 +356,10 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO } } final PreparedStatement psm = con.prepareStatement(template.createText()); - final ParameterMetaData pmd = psm.getParameterMetaData(); - for (int i = 1; i <= pmd.getParameterCount(); ++i) + final ParameterTypeProvider ptp = + new ParameterTypeProvider(template, indexMap, + psm.getParameterMetaData()); + for (int i = 1; i <= ptp.getParameterCount(); ++i) { final Entry<String, String> entry = indexMap.get(i - 1); if (entry == null) @@ -242,7 +369,7 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO final String strValue = entry.getValue(); try { - switch (pmd.getParameterType(i)) + switch (ptp.getParameterType(i)) { case Types.BIT: case Types.BOOLEAN: @@ -277,9 +404,14 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO case Types.NCHAR: case Types.NVARCHAR: case Types.LONGNVARCHAR: - case Types.ARRAY: psm.setString(i, strValue); break; + case Types.ARRAY: + psm.setString( + i, + arrayVariableNames.contains(entry.getKey()) ? "{" + + strValue + "}" : strValue); + break; case Types.TIME: psm.setTime(i, Time.valueOf(strValue)); break; @@ -291,8 +423,8 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO break; default: throw new SQLDataException("Unsupported SQL type " - + pmd.getParameterTypeName(i) + "(" - + pmd.getParameterType(i) + ") for variable " + + ptp.getParameterTypeName(i) + "(" + + ptp.getParameterType(i) + ") for variable " + entry.getKey()); } } catch (RuntimeException ex) @@ -303,6 +435,14 @@ class DAO extends SimpleJdbcDaoSupport implements IDAO } return psm; } + + private void addToSet(List<IToken> tokens, Set<String> nameSet) + { + for (IToken token : tokens) + { + nameSet.add(token.tryGetName()); + } + } }; } } diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/query/server/DAOTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/query/server/DAOTest.java index c0f2c4ba336..4b53c0f4cfc 100644 --- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/query/server/DAOTest.java +++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/plugin/query/server/DAOTest.java @@ -94,7 +94,37 @@ public class DAOTest extends AbstractTransactionalTestNGSpringContextTests } @Test - public void testQueryWithArrayBinding() + public void testQueryWithStringBinding() + { + String query = + "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code like ${code} order by id"; + QueryParameterBindings bindings = new QueryParameterBindings(); + bindings.addBinding("code", "200811050921591%"); + testQueryWithBindings(query, bindings); + } + + @Test + public void testQueryWithStringBindingExplicitType() + { + String query = + "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code like ${code::string} order by id"; + QueryParameterBindings bindings = new QueryParameterBindings(); + bindings.addBinding("code", "200811050921591%"); + testQueryWithBindings(query, bindings); + } + + @Test + public void testQueryWithStringBindingOldStyle() + { + String query = + "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code like '${code}' order by id"; + QueryParameterBindings bindings = new QueryParameterBindings(); + bindings.addBinding("code", "200811050921591%"); + testQueryWithBindings(query, bindings); + } + + @Test + public void testQueryWithPostgreSQLArrayBinding() { String query = "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code = any(${codes}::text[]) order by id"; @@ -103,6 +133,26 @@ public class DAOTest extends AbstractTransactionalTestNGSpringContextTests testQueryWithBindings(query, bindings); } + @Test + public void testQueryWithPostgreSQLArrayBindingOldStyle() + { + String query = + "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code = any('{${codes}}'::text[]) order by id"; + QueryParameterBindings bindings = new QueryParameterBindings(); + bindings.addBinding("codes", "20081105092159188-3, 20081105092159111-1"); + testQueryWithBindings(query, bindings); + } + + @Test + public void testQueryWithPostgreSQLArrayBindingSimplified() + { + String query = + "select id, code as DATA_SET_KEY, registration_timestamp, is_valid from data where code = any({${codes}}) order by id"; + QueryParameterBindings bindings = new QueryParameterBindings(); + bindings.addBinding("codes", "20081105092159188-3, 20081105092159111-1"); + testQueryWithBindings(query, bindings); + } + @Test(expectedExceptions = DataIntegrityViolationException.class) public void testQueryWithBindingsSQLInjection() { -- GitLab