diff --git a/amazon-redshift-plugin/src/main/java/io/cdap/plugin/amazon/redshift/RedshiftConnector.java b/amazon-redshift-plugin/src/main/java/io/cdap/plugin/amazon/redshift/RedshiftConnector.java index fb8cac4a7..57a253ebb 100644 --- a/amazon-redshift-plugin/src/main/java/io/cdap/plugin/amazon/redshift/RedshiftConnector.java +++ b/amazon-redshift-plugin/src/main/java/io/cdap/plugin/amazon/redshift/RedshiftConnector.java @@ -82,7 +82,7 @@ protected SchemaReader getSchemaReader(String sessionID) { @Override protected String getTableName(String database, String schema, String table) { - return String.format("\"%s\".\"%s\"", schema, table); + return quoteIdentifier(schema, '"') + "." + quoteIdentifier(table, '"'); } @Override diff --git a/amazon-redshift-plugin/src/test/java/io/cdap/plugin/amazon/redshift/RedshiftConnectorUnitTest.java b/amazon-redshift-plugin/src/test/java/io/cdap/plugin/amazon/redshift/RedshiftConnectorUnitTest.java index 94ea6004b..16c94a3f0 100644 --- a/amazon-redshift-plugin/src/test/java/io/cdap/plugin/amazon/redshift/RedshiftConnectorUnitTest.java +++ b/amazon-redshift-plugin/src/test/java/io/cdap/plugin/amazon/redshift/RedshiftConnectorUnitTest.java @@ -41,6 +41,15 @@ public void getTableNameTest() { CONNECTOR.getTableName("db", "schema", "table")); } + /** + * Unit test for getTableName() with identifiers containing double quotes + */ + @Test + public void getTableNameEscapesQuotesTest() { + Assert.assertEquals("\"my\"\"schema\".\"t\"\"; DELETE FROM x; --\"", + CONNECTOR.getTableName("db", "my\"schema", "t\"; DELETE FROM x; --")); + } + @Test public void getRandomQuery() { Assert.assertEquals("SELECT * FROM TestData\n" + diff --git a/cloudsql-mysql-plugin/src/main/java/io/cdap/plugin/cloudsql/mysql/CloudSQLMySQLConnector.java b/cloudsql-mysql-plugin/src/main/java/io/cdap/plugin/cloudsql/mysql/CloudSQLMySQLConnector.java index 0751bd160..dccb8b047 100644 --- a/cloudsql-mysql-plugin/src/main/java/io/cdap/plugin/cloudsql/mysql/CloudSQLMySQLConnector.java +++ b/cloudsql-mysql-plugin/src/main/java/io/cdap/plugin/cloudsql/mysql/CloudSQLMySQLConnector.java @@ -82,7 +82,7 @@ protected SchemaReader getSchemaReader(String sessionID) { @Override protected String getTableName(String database, String schema, String table) { - return String.format("`%s`.`%s`", database, table); + return quoteIdentifier(database, '`') + "." + quoteIdentifier(table, '`'); } @Override diff --git a/cloudsql-postgresql-plugin/src/main/java/io/cdap/plugin/cloudsql/postgres/CloudSQLPostgreSQLConnector.java b/cloudsql-postgresql-plugin/src/main/java/io/cdap/plugin/cloudsql/postgres/CloudSQLPostgreSQLConnector.java index 348a1b94c..8f1921478 100644 --- a/cloudsql-postgresql-plugin/src/main/java/io/cdap/plugin/cloudsql/postgres/CloudSQLPostgreSQLConnector.java +++ b/cloudsql-postgresql-plugin/src/main/java/io/cdap/plugin/cloudsql/postgres/CloudSQLPostgreSQLConnector.java @@ -86,7 +86,7 @@ protected SchemaReader getSchemaReader(String sessionID) { @Override protected String getTableName(String database, String schema, String table) { - return String.format("\"%s\".\"%s\"", schema, table); + return quoteIdentifier(schema, '"') + "." + quoteIdentifier(table, '"'); } @Override diff --git a/database-commons/pom.xml b/database-commons/pom.xml index 98bcb3803..2d92db70d 100644 --- a/database-commons/pom.xml +++ b/database-commons/pom.xml @@ -77,6 +77,10 @@ dev.failsafe failsafe + + com.github.jsqlparser + jsqlparser + diff --git a/database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBSpecificConnector.java b/database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBSpecificConnector.java index e0407203b..f1b0fdf6f 100644 --- a/database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBSpecificConnector.java +++ b/database-commons/src/main/java/io/cdap/plugin/db/connector/AbstractDBSpecificConnector.java @@ -150,8 +150,17 @@ protected String getConnectionString(String database) { } protected String getTableName(String database, String schema, String table) { - return schema == null ? String.format("\"%s\".\"%s\"", database, table) - : String.format("\"%s\".\"%s\".\"%s\"", database, schema, table); + return schema == null ? quoteIdentifier(database, '"') + "." + quoteIdentifier(table, '"') + : quoteIdentifier(database, '"') + "." + quoteIdentifier(schema, '"') + "." + quoteIdentifier(table, '"'); + } + + /** + * Wraps the given identifier in the given quote character, doubling any embedded quote characters so that a + * user supplied database, schema, table or column name can never break out of the quoted identifier. + */ + public static String quoteIdentifier(String identifier, char quote) { + String quoteString = String.valueOf(quote); + return quoteString + identifier.replace(quoteString, quoteString + quoteString) + quoteString; } protected String getTableQuery(String database, String schema, String table) { diff --git a/database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java b/database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java index 908d27eb9..53f26c617 100644 --- a/database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java +++ b/database-commons/src/main/java/io/cdap/plugin/db/source/AbstractDBSource.java @@ -52,6 +52,7 @@ import io.cdap.plugin.util.DriverCleanup; import io.cdap.plugin.util.RetryPolicyUtil; import io.cdap.plugin.util.RetryUtils; +import io.cdap.plugin.util.SelectQueryValidator; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.mapreduce.MRJobConfig; import org.apache.hadoop.mapreduce.lib.db.DBConfiguration; @@ -106,6 +107,7 @@ public void configurePipeline(PipelineConfigurer pipelineConfigurer) { StageConfigurer stageConfigurer = pipelineConfigurer.getStageConfigurer(); FailureCollector collector = stageConfigurer.getFailureCollector(); sourceConfig.validate(collector); + boolean queriesValid = validateQueries(collector); if (sourceConfig.getSchema() != null) { stageConfigurer.setOutputSchema(sourceConfig.getSchema()); return; @@ -119,7 +121,8 @@ public void configurePipeline(PipelineConfigurer pipelineConfigurer) { Class driverClass = DBUtils.getDriverClass( pipelineConfigurer, sourceConfig, ConnectionConfig.JDBC_PLUGIN_TYPE); - if (sourceConfig.canConnect()) { + // never run a query that failed the read-only validation, not even to fetch its schema + if (sourceConfig.canConnect() && queriesValid) { try { stageConfigurer.setOutputSchema(getSchema(driverClass)); } catch (IllegalAccessException | InstantiationException e) { @@ -228,6 +231,43 @@ protected String getErrorDetailsProviderClassName() { return null; } + /** + * Returns whether square brackets quote identifiers in the database's SQL dialect, as in SQL Server. + * Override this method to allow such identifiers in the import and bounding queries. + */ + protected boolean useSquareBracketQuotes() { + return false; + } + + /** + * Validates that the import and bounding queries are single read-only SELECT statements, since they are executed + * with the credential of the connection. Queries that are macros are validated at runtime, once they are evaluated. + * + * @return true if the queries are valid or cannot be validated yet + */ + private boolean validateQueries(FailureCollector collector) { + boolean importQueryValid = validateQuery(DBSourceConfig.IMPORT_QUERY, "Import Query", + sourceConfig.getImportQuery(), collector); + boolean boundingQueryValid = validateQuery(DBSourceConfig.BOUNDING_QUERY, "Bounding Query", + sourceConfig.getBoundingQuery(), collector); + return importQueryValid && boundingQueryValid; + } + + private boolean validateQuery(String property, String label, @Nullable String query, FailureCollector collector) { + if (sourceConfig.containsMacro(property) || Strings.isNullOrEmpty(query)) { + return true; + } + try { + SelectQueryValidator.validate(query, useSquareBracketQuotes()); + return true; + } catch (IllegalArgumentException e) { + collector.addFailure(String.format("Invalid %s: %s", label, e.getMessage()), + "Provide a single SELECT statement that only reads data.") + .withConfigProperty(property); + return false; + } + } + /** * Returns the DBErrorDetailsProvider instance. * Override this method to provide a custom DBErrorDetailsProvider instance. @@ -272,6 +312,7 @@ private Connection getConnection() { public void prepareRun(BatchSourceContext context) throws Exception { FailureCollector collector = context.getFailureCollector(); sourceConfig.validate(collector); + validateQueries(collector); collector.getOrThrowException(); LOG.debug("pluginType = {}; pluginName = {}; connectionString = {}; importQuery = {}; " + diff --git a/databricks-plugin/src/main/java/io/cdap/plugin/databricks/DatabricksConnector.java b/databricks-plugin/src/main/java/io/cdap/plugin/databricks/DatabricksConnector.java index c78ceca6f..5997cf627 100644 --- a/databricks-plugin/src/main/java/io/cdap/plugin/databricks/DatabricksConnector.java +++ b/databricks-plugin/src/main/java/io/cdap/plugin/databricks/DatabricksConnector.java @@ -116,15 +116,15 @@ protected SchemaReader getSchemaReader(String sessionID) { @Override protected String getTableName(String database, String schema, String table) { if (database == null && schema == null) { - return String.format("`%s`", table); + return quoteIdentifier(table, '`'); } if (database == null) { - return String.format("`%s`.`%s`", schema, table); + return quoteIdentifier(schema, '`') + "." + quoteIdentifier(table, '`'); } if (schema == null) { - return String.format("`%s`.`%s`", database, table); + return quoteIdentifier(database, '`') + "." + quoteIdentifier(table, '`'); } - return String.format("`%s`.`%s`.`%s`", database, schema, table); + return quoteIdentifier(database, '`') + "." + quoteIdentifier(schema, '`') + "." + quoteIdentifier(table, '`'); } @Override @@ -136,6 +136,7 @@ protected String getRandomQuery(String tableName, int limit) { @Override protected String getStratifiedQuery(String tableName, int limit, String strata, String sessionID) { + String strataColumn = quoteIdentifier(strata, '`'); return String.format("WITH t_%s AS (\n" + " SELECT *,\n" + " ROW_NUMBER() OVER (ORDER BY %s, RAND()) AS sqn_%s,\n" + @@ -146,8 +147,8 @@ protected String getStratifiedQuery(String tableName, int limit, String strata, "WHERE MOD(sqn_%s, GREATEST(1, CAST(c_%s / %d AS BIGINT))) = 1\n" + "ORDER BY %s\n" + "LIMIT %d", - sessionID, strata, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, - limit, strata, limit); + sessionID, strataColumn, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, + limit, strataColumn, limit); } @Override diff --git a/databricks-plugin/src/test/java/io/cdap/plugin/databricks/DatabricksConnectorUnitTest.java b/databricks-plugin/src/test/java/io/cdap/plugin/databricks/DatabricksConnectorUnitTest.java index a04dfb398..27b830c76 100644 --- a/databricks-plugin/src/test/java/io/cdap/plugin/databricks/DatabricksConnectorUnitTest.java +++ b/databricks-plugin/src/test/java/io/cdap/plugin/databricks/DatabricksConnectorUnitTest.java @@ -38,6 +38,21 @@ public void testGetTableName() { CONNECTOR.getTableName(null, null, "my_table")); } + @Test + public void testGetTableNameEscapesBackticks() { + Assert.assertEquals("`main`.`default`.`t``; DELETE FROM x; --`", + CONNECTOR.getTableName("main", "default", "t`; DELETE FROM x; --")); + Assert.assertEquals("`my``schema`.`my``table`", + CONNECTOR.getTableName(null, "my`schema", "my`table")); + } + + @Test + public void testGetStratifiedQueryEscapesStrata() { + String query = CONNECTOR.getStratifiedQuery("`main`.`default`.`my_table`", 10, "id`; DROP TABLE t; --", "s1"); + Assert.assertTrue(query.contains("ORDER BY `id``; DROP TABLE t; --`, RAND()")); + Assert.assertTrue(query.contains("ORDER BY `id``; DROP TABLE t; --`\n")); + } + @Test public void testGetRandomQuery() { Assert.assertEquals("SELECT * FROM `main`.`default`.`my_table`\n" + @@ -49,13 +64,13 @@ public void testGetRandomQuery() { public void testGetStratifiedQuery() { Assert.assertEquals("WITH t_s1 AS (\n" + " SELECT *,\n" + - " ROW_NUMBER() OVER (ORDER BY id, RAND()) AS sqn_s1,\n" + + " ROW_NUMBER() OVER (ORDER BY `id`, RAND()) AS sqn_s1,\n" + " COUNT(*) OVER () AS c_s1\n" + " FROM `main`.`default`.`my_table`\n" + " )\n" + "SELECT * FROM t_s1\n" + "WHERE MOD(sqn_s1, GREATEST(1, CAST(c_s1 / 10 AS BIGINT))) = 1\n" + - "ORDER BY id\n" + + "ORDER BY `id`\n" + "LIMIT 10", CONNECTOR.getStratifiedQuery("`main`.`default`.`my_table`", 10, "id", "s1")); } diff --git a/generic-database-plugin/src/main/java/io/cdap/plugin/jdbc/DatabaseSource.java b/generic-database-plugin/src/main/java/io/cdap/plugin/jdbc/DatabaseSource.java index b35b025b7..1c07b2d8a 100644 --- a/generic-database-plugin/src/main/java/io/cdap/plugin/jdbc/DatabaseSource.java +++ b/generic-database-plugin/src/main/java/io/cdap/plugin/jdbc/DatabaseSource.java @@ -46,6 +46,14 @@ protected String createConnectionString() { return databaseSourceConfig.connectionString; } + @Override + protected boolean useSquareBracketQuotes() { + // SQL Server quotes identifiers with square brackets + String connectionString = databaseSourceConfig.connectionString; + return connectionString != null && (connectionString.toLowerCase().startsWith("jdbc:sqlserver:") + || connectionString.toLowerCase().startsWith("jdbc:jtds:sqlserver:")); + } + /** * Generic database source configuration. */ diff --git a/generic-database-plugin/src/test/java/io/cdap/plugin/db/sink/DBSourceTestRun.java b/generic-database-plugin/src/test/java/io/cdap/plugin/db/sink/DBSourceTestRun.java index bedb720a2..f11342461 100644 --- a/generic-database-plugin/src/test/java/io/cdap/plugin/db/sink/DBSourceTestRun.java +++ b/generic-database-plugin/src/test/java/io/cdap/plugin/db/sink/DBSourceTestRun.java @@ -20,16 +20,19 @@ import io.cdap.cdap.api.common.Bytes; import io.cdap.cdap.api.data.format.StructuredRecord; import io.cdap.cdap.api.dataset.table.Table; +import io.cdap.cdap.datapipeline.SmartWorkflow; import io.cdap.cdap.etl.api.batch.BatchSource; import io.cdap.cdap.etl.mock.batch.MockSink; import io.cdap.cdap.etl.proto.v2.ETLBatchConfig; import io.cdap.cdap.etl.proto.v2.ETLPlugin; import io.cdap.cdap.etl.proto.v2.ETLStage; +import io.cdap.cdap.proto.ProgramRunStatus; import io.cdap.cdap.proto.artifact.AppRequest; import io.cdap.cdap.proto.id.ApplicationId; import io.cdap.cdap.proto.id.NamespaceId; import io.cdap.cdap.test.ApplicationManager; import io.cdap.cdap.test.DataSetManager; +import io.cdap.cdap.test.WorkflowManager; import io.cdap.plugin.GenericDatabasePluginTestBase; import io.cdap.plugin.common.Constants; import io.cdap.plugin.db.ConnectionConfig; @@ -41,7 +44,11 @@ import java.math.BigDecimal; import java.math.MathContext; import java.nio.ByteBuffer; +import java.sql.Connection; import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.sql.Time; import java.text.SimpleDateFormat; import java.time.LocalDate; @@ -52,6 +59,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Test for ETL using databases. @@ -329,6 +337,61 @@ public void testUserNamePasswordCombinations() throws Exception { deployApplication(appId, appRequest); } + @Test + public void testNonSelectImportQueryFailsDeployment() throws Exception { + int rowCount = countRows("my_table"); + String importQuery = "SELECT * FROM \"my_table\" WHERE $CONDITIONS; DELETE FROM \"my_table\""; + ETLPlugin sourceConfig = new ETLPlugin( + DatabaseConstants.PLUGIN_NAME, + BatchSource.PLUGIN_TYPE, + ImmutableMap.builder() + .put(ConnectionConfig.CONNECTION_STRING, getConnectionURL()) + .put(ConnectionConfig.JDBC_PLUGIN_NAME, JDBC_DRIVER_NAME) + .put(AbstractDBSource.DBSourceConfig.IMPORT_QUERY, importQuery) + .put(AbstractDBSource.DBSourceConfig.BOUNDING_QUERY, "SELECT MIN(ID),MAX(ID) from \"my_table\"") + .put(AbstractDBSource.DBSourceConfig.SPLIT_BY, "ID") + .put(Constants.Reference.REFERENCE_NAME, "DBNonSelectQueryTest") + .build(), + null); + ETLBatchConfig etlConfig = getETLBatchConfig(sourceConfig, MockSink.getPlugin("nonSelectQueryOutput")); + assertDeploymentFailure(NamespaceId.DEFAULT.app("dbSourceNonSelectQueryTest"), etlConfig, DATAPIPELINE_ARTIFACT, + "Deploying DB Source with a non SELECT import query should have failed."); + Assert.assertEquals(rowCount, countRows("my_table")); + } + + @Test + public void testNonSelectImportQueryMacroFailsRun() throws Exception { + int rowCount = countRows("my_table"); + ETLPlugin sourceConfig = new ETLPlugin( + DatabaseConstants.PLUGIN_NAME, + BatchSource.PLUGIN_TYPE, + ImmutableMap.builder() + .put(ConnectionConfig.CONNECTION_STRING, getConnectionURL()) + .put(ConnectionConfig.JDBC_PLUGIN_NAME, JDBC_DRIVER_NAME) + .put(AbstractDBSource.DBSourceConfig.IMPORT_QUERY, "${importQuery}") + .put(AbstractDBSource.DBSourceConfig.BOUNDING_QUERY, "SELECT MIN(ID),MAX(ID) from \"my_table\"") + .put(AbstractDBSource.DBSourceConfig.SPLIT_BY, "ID") + .put(Constants.Reference.REFERENCE_NAME, "DBNonSelectQueryMacroTest") + .build(), + null); + ApplicationManager appManager = deployETL(sourceConfig, MockSink.getPlugin("nonSelectQueryMacroOutput"), + DATAPIPELINE_ARTIFACT, "dbSourceNonSelectQueryMacroTest"); + WorkflowManager workflowManager = appManager.getWorkflowManager(SmartWorkflow.NAME); + workflowManager.start(ImmutableMap.of( + "importQuery", "SELECT * FROM \"my_table\" WHERE $CONDITIONS; DELETE FROM \"my_table\"")); + workflowManager.waitForRun(ProgramRunStatus.FAILED, 5, TimeUnit.MINUTES); + Assert.assertEquals(rowCount, countRows("my_table")); + } + + private int countRows(String table) throws SQLException { + try (Connection connection = getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(String.format("SELECT COUNT(*) FROM \"%s\"", table))) { + resultSet.next(); + return resultSet.getInt(1); + } + } + @Test public void testNonExistentDBTable() throws Exception { // source diff --git a/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerConnector.java b/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerConnector.java index 7e86d97ce..4f3ba8963 100644 --- a/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerConnector.java +++ b/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerConnector.java @@ -117,7 +117,8 @@ public StructuredRecord transform(LongWritable longWritable, SqlServerSourceDBRe @Override protected String getTableName(String database, String schema, String table) { - return String.format("\"%s\".\"%s\".\"%s\"", database, schema, table); + return quoteIdentifier(database, '"') + "." + quoteIdentifier(schema, '"') + "." + + quoteIdentifier(table, '"'); } @Override @@ -137,6 +138,7 @@ protected String getRandomQuery(String tableName, int limit) { @Override protected String getStratifiedQuery(String tableName, int limit, String strata, String sessionID) { + String strataColumn = quoteIdentifier(strata, '"'); return String.format("WITH t_%s AS (\n" + " SELECT *,\n" + " ROW_NUMBER() OVER (ORDER BY %s, RAND()) AS sqn_%s,\n" + @@ -146,8 +148,8 @@ protected String getStratifiedQuery(String tableName, int limit, String strata, "SELECT TOP %d * FROM t_%s\n" + "WHERE sqn_%s %% CAST(0.5 * ((c_%s / %d + 1) + ABS(c_%s / %d - 1)) AS bigint) = 1\n" + "ORDER BY %s", - sessionID, strata, sessionID, sessionID, tableName, limit, sessionID, sessionID, sessionID, - limit, sessionID, limit, strata); + sessionID, strataColumn, sessionID, sessionID, tableName, limit, sessionID, sessionID, + sessionID, limit, sessionID, limit, strataColumn); } @Override diff --git a/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerSource.java b/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerSource.java index d8d4cf39e..b6bcb9ff9 100644 --- a/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerSource.java +++ b/mssql-plugin/src/main/java/io/cdap/plugin/mssql/SqlServerSource.java @@ -81,6 +81,11 @@ protected String getErrorDetailsProviderClassName() { return SqlServerErrorDetailsProvider.class.getName(); } + @Override + protected boolean useSquareBracketQuotes() { + return true; + } + @Override protected DBErrorDetailsProvider getErrorDetailsProvider() { if (dbErrorDetailsProvider == null) { diff --git a/mssql-plugin/src/test/java/io/cdap/plugin/mssql/SqlServerConnectorUnitTest.java b/mssql-plugin/src/test/java/io/cdap/plugin/mssql/SqlServerConnectorUnitTest.java index 0163cd55e..231d072b9 100644 --- a/mssql-plugin/src/test/java/io/cdap/plugin/mssql/SqlServerConnectorUnitTest.java +++ b/mssql-plugin/src/test/java/io/cdap/plugin/mssql/SqlServerConnectorUnitTest.java @@ -34,6 +34,27 @@ public class SqlServerConnectorUnitTest { // private static final SqlServerConnector CONNECTOR = new SqlServerConnector(new SqlServerConnectorConfig()); + /** + * Unit test for getTableName() with identifiers containing double quotes + */ + @Test + public void getTableNameEscapesQuotesTest() { + Assert.assertEquals("\"db\".\"dbo\".\"t\"\"; DELETE FROM x; --\"", + CONNECTOR.getTableName("db", "dbo", "t\"; DELETE FROM x; --")); + Assert.assertEquals("\"my\"\"db\".\"my\"\"schema\".\"my\"\"table\"", + CONNECTOR.getTableName("my\"db", "my\"schema", "my\"table")); + } + + /** + * Unit test for getStratifiedQuery() with a strata column containing double quotes + */ + @Test + public void getStratifiedQueryEscapesStrataTest() { + String query = CONNECTOR.getStratifiedQuery("\"db\".\"dbo\".\"table\"", 10, "id\"; DROP TABLE t; --", "s1"); + Assert.assertTrue(query.contains("ORDER BY \"id\"\"; DROP TABLE t; --\", RAND()")); + Assert.assertTrue(query.endsWith("ORDER BY \"id\"\"; DROP TABLE t; --\"")); + } + /** * Unit tests for getTableQuery() */ diff --git a/mysql-plugin/src/main/java/io/cdap/plugin/mysql/MysqlConnector.java b/mysql-plugin/src/main/java/io/cdap/plugin/mysql/MysqlConnector.java index 214d8650e..0f854bc88 100644 --- a/mysql-plugin/src/main/java/io/cdap/plugin/mysql/MysqlConnector.java +++ b/mysql-plugin/src/main/java/io/cdap/plugin/mysql/MysqlConnector.java @@ -98,7 +98,7 @@ protected void setConnectorSpec(ConnectorSpecRequest request, DBConnectorPath pa @Override protected String getTableName(String database, String schema, String table) { - return String.format("`%s`.`%s`", database, table); + return quoteIdentifier(database, '`') + "." + quoteIdentifier(table, '`'); } @Override @@ -112,6 +112,7 @@ protected String getRandomQuery(String tableName, int limit) { @Override protected String getStratifiedQuery(String tableName, int limit, String strata, String sessionID) { + String strataColumn = quoteIdentifier(strata, '`'); return String.format("WITH t_%s AS (\n" + " SELECT *,\n" + " ROW_NUMBER() OVER (ORDER BY %s, RAND()) AS sqn_%s,\n" + @@ -122,8 +123,8 @@ protected String getStratifiedQuery(String tableName, int limit, String strata, "WHERE MOD(sqn_%s, GREATEST(1, CAST(c_%s / %d AS UNSIGNED))) = 1\n" + "ORDER BY %s\n" + "LIMIT %d", - sessionID, strata, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, - limit, strata, limit); + sessionID, strataColumn, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, + limit, strataColumn, limit); } @Override diff --git a/mysql-plugin/src/test/java/io/cdap/plugin/mysql/MysqlConnectorUnitTest.java b/mysql-plugin/src/test/java/io/cdap/plugin/mysql/MysqlConnectorUnitTest.java index df731d194..01820842e 100644 --- a/mysql-plugin/src/test/java/io/cdap/plugin/mysql/MysqlConnectorUnitTest.java +++ b/mysql-plugin/src/test/java/io/cdap/plugin/mysql/MysqlConnectorUnitTest.java @@ -42,6 +42,25 @@ public void getTableNameTest() { CONNECTOR.getTableName("db", "schema", "table")); } + /** + * Unit test for getTableName() with identifiers containing backticks + */ + @Test + public void getTableNameEscapesBackticksTest() { + Assert.assertEquals("`my``db`.`t``; DELETE FROM x; --`", + CONNECTOR.getTableName("my`db", "schema", "t`; DELETE FROM x; --")); + } + + /** + * Unit test for getStratifiedQuery() with a strata column containing backticks + */ + @Test + public void getStratifiedQueryEscapesStrataTest() { + String query = CONNECTOR.getStratifiedQuery("`db`.`table`", 10, "id`; DROP TABLE t; --", "s1"); + Assert.assertTrue(query.contains("ORDER BY `id``; DROP TABLE t; --`, RAND()")); + Assert.assertTrue(query.contains("ORDER BY `id``; DROP TABLE t; --`\n")); + } + /** * Unit tests for getTableQuery() */ diff --git a/oracle-plugin/src/main/java/io/cdap/plugin/oracle/OracleConnector.java b/oracle-plugin/src/main/java/io/cdap/plugin/oracle/OracleConnector.java index 469ca45f2..a59c0688e 100644 --- a/oracle-plugin/src/main/java/io/cdap/plugin/oracle/OracleConnector.java +++ b/oracle-plugin/src/main/java/io/cdap/plugin/oracle/OracleConnector.java @@ -135,7 +135,7 @@ protected String getConnectionString(@Nullable String database) { @Override protected String getTableName(String database, String schema, String table) { - return String.format("\"%s\".\"%s\"", schema, table); + return quoteIdentifier(schema, '"') + "." + quoteIdentifier(table, '"'); } @Override @@ -157,6 +157,7 @@ protected String getRandomQuery(String tableName, int limit) { @Override protected String getStratifiedQuery(String tableName, int limit, String strata, String sessionID) { + String strataColumn = quoteIdentifier(strata, '"'); return String.format("WITH t_%s AS (\n" + " SELECT %s.*,\n" + " ROW_NUMBER() OVER (ORDER BY %s, dbms_random.random) AS s_%s,\n" + @@ -166,8 +167,8 @@ protected String getStratifiedQuery(String tableName, int limit, String strata, " )\n" + "SELECT * FROM t_%s\n" + "WHERE MOD(s_%s, GREATEST(1, CAST(c_%s / %d AS INTEGER))) = 1 AND ROWNUM <= %d", - sessionID, tableName, strata, sessionID, tableName, sessionID, tableName, - strata, sessionID, sessionID, sessionID, limit, limit); + sessionID, tableName, strataColumn, sessionID, tableName, sessionID, tableName, + strataColumn, sessionID, sessionID, sessionID, limit, limit); } @Override diff --git a/oracle-plugin/src/test/java/io/cdap/plugin/oracle/OracleConnectorUnitTest.java b/oracle-plugin/src/test/java/io/cdap/plugin/oracle/OracleConnectorUnitTest.java index f89a6c1d7..2441862d0 100644 --- a/oracle-plugin/src/test/java/io/cdap/plugin/oracle/OracleConnectorUnitTest.java +++ b/oracle-plugin/src/test/java/io/cdap/plugin/oracle/OracleConnectorUnitTest.java @@ -37,6 +37,25 @@ public void getTableNameTest() { CONNECTOR.getTableName("db", "schema", "table")); } + /** + * Unit test for getTableName() with identifiers containing double quotes + */ + @Test + public void getTableNameEscapesQuotesTest() { + Assert.assertEquals("\"MY\"\"SCHEMA\".\"T\"\" WHERE 1=1 --\"", + CONNECTOR.getTableName("db", "MY\"SCHEMA", "T\" WHERE 1=1 --")); + } + + /** + * Unit test for getStratifiedQuery() with a strata column containing double quotes + */ + @Test + public void getStratifiedQueryEscapesStrataTest() { + String query = CONNECTOR.getStratifiedQuery("\"SCHEMA\".\"TABLE\"", 10, "ID\", 1) --", "s1"); + Assert.assertTrue(query.contains("ORDER BY \"ID\"\", 1) --\", dbms_random.random")); + Assert.assertTrue(query.contains("ORDER BY \"ID\"\", 1) --\" )")); + } + /** * Unit tests for getTableQuery() */ diff --git a/pom.xml b/pom.xml index c739d8855..a04552767 100644 --- a/pom.xml +++ b/pom.xml @@ -310,6 +310,12 @@ failsafe 3.3.2 + + + com.github.jsqlparser + jsqlparser + 4.9 + org.hsqldb hsqldb diff --git a/postgresql-plugin/src/main/java/io/cdap/plugin/postgres/PostgresConnector.java b/postgresql-plugin/src/main/java/io/cdap/plugin/postgres/PostgresConnector.java index 5b23ce6cd..4ad703b61 100644 --- a/postgresql-plugin/src/main/java/io/cdap/plugin/postgres/PostgresConnector.java +++ b/postgresql-plugin/src/main/java/io/cdap/plugin/postgres/PostgresConnector.java @@ -117,7 +117,7 @@ public StructuredRecord transform(LongWritable longWritable, PostgresDBRecord re @Override protected String getTableName(String database, String schema, String table) { - return String.format("\"%s\".\"%s\"", schema, table); + return quoteIdentifier(schema, '"') + "." + quoteIdentifier(table, '"'); } @Override @@ -129,6 +129,7 @@ protected String getRandomQuery(String tableName, int limit) { @Override protected String getStratifiedQuery(String tableName, int limit, String strata, String sessionID) { + String strataColumn = quoteIdentifier(strata, '"'); // this query times out on larger datasets; would benefit from optimization return String.format("WITH t_%s AS (\n" + " SELECT *,\n" + @@ -140,8 +141,8 @@ protected String getStratifiedQuery(String tableName, int limit, String strata, "WHERE mod(sqn_%s, GREATEST(1, CAST(c_%s / %d AS bigint))) = 1\n" + "ORDER BY %s\n" + "LIMIT %d", - sessionID, strata, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, - limit, strata, limit); + sessionID, strataColumn, sessionID, sessionID, tableName, sessionID, sessionID, sessionID, + limit, strataColumn, limit); } @Override diff --git a/postgresql-plugin/src/test/java/io/cdap/plugin/postgres/PostgresConnectorUnitTest.java b/postgresql-plugin/src/test/java/io/cdap/plugin/postgres/PostgresConnectorUnitTest.java index f286cf574..57e6399d4 100644 --- a/postgresql-plugin/src/test/java/io/cdap/plugin/postgres/PostgresConnectorUnitTest.java +++ b/postgresql-plugin/src/test/java/io/cdap/plugin/postgres/PostgresConnectorUnitTest.java @@ -41,6 +41,28 @@ public void getTableNameTest() { CONNECTOR.getTableName("db", "schema", "table")); } + /** + * Unit test for getTableName() with identifiers containing double quotes + */ + @Test + public void getTableNameEscapesQuotesTest() { + Assert.assertEquals("\"pg_catalog\".\"pg_class\"\" LIMIT 0; UPDATE t SET v='x'; SELECT * FROM \"\"pg_class\"", + CONNECTOR.getTableName("db", "pg_catalog", + "pg_class\" LIMIT 0; UPDATE t SET v='x'; SELECT * FROM \"pg_class")); + Assert.assertEquals("\"my\"\"schema\".\"my\"\"table\"", + CONNECTOR.getTableName("db", "my\"schema", "my\"table")); + } + + /** + * Unit test for getStratifiedQuery() with a strata column containing double quotes + */ + @Test + public void getStratifiedQueryEscapesStrataTest() { + String query = CONNECTOR.getStratifiedQuery("\"schema\".\"table\"", 10, "id\"; DROP TABLE t; --", "s1"); + Assert.assertTrue(query.contains("ORDER BY \"id\"\"; DROP TABLE t; --\", random()")); + Assert.assertTrue(query.contains("ORDER BY \"id\"\"; DROP TABLE t; --\"\n")); + } + /** * Unit tests for getTableQuery() */