From a093dec7322e76c7c912389f52d61b51c19deefc Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:16:06 +0800 Subject: [PATCH 1/2] fix(mongodb): escape SQL identifiers and literals in metadata/DDL paths (#1914) --- .../chat2db-community-mongodb/pom.xml | 5 + .../plugin/mongodb/MongodbDBManager.java | 9 +- .../plugin/mongodb/MongodbMetaData.java | 6 +- .../plugin/mongodb/MongodbScriptExecutor.java | 2 +- .../plugin/mongodb/MongodbSqlBuilder.java | 13 +- .../plugin/mongodb/MongodbSqlEscapes.java | 71 ++++++++++ .../plugin/mongodb/MongodbSqlEscapesTest.java | 128 ++++++++++++++++++ 7 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java create mode 100644 chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/pom.xml b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/pom.xml index 70e02a35a..209f9b89d 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/pom.xml +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/pom.xml @@ -15,6 +15,11 @@ ai.chat2db chat2db-community-spi + + org.junit.jupiter + junit-jupiter + test + diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java index 0990cd651..2c4d3dd64 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java @@ -29,7 +29,7 @@ public void connectDatabase(Connection connection, String database) { return; } try { - DefaultSQLExecutor.getInstance().execute(connection, String.format(SCRIPT_USE_SCHEMA, schemaName)); + DefaultSQLExecutor.getInstance().execute(connection, String.format(SCRIPT_USE_SCHEMA, MongodbSqlEscapes.requireMongoName(schemaName, "database name"))); } catch (SQLException e) { throw new RuntimeException(e); } @@ -37,17 +37,18 @@ public void connectDatabase(Connection connection, String database) { @Override public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) { - return String.format(SCRIPT_DROP_COLLECTION, tableName); + return String.format(SCRIPT_DROP_COLLECTION, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); } @Override public String truncateTable(Connection connection, String databaseName, String schemaName, String tableName) throws SQLException { - return String.format(SCRIPT_TRUNCATE_COLLECTION, tableName); + return String.format(SCRIPT_TRUNCATE_COLLECTION, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); } @Override public void copyTable(Connection connection, String databaseName, String schemaName, String tableName, String newTableName,boolean copyData) throws SQLException { - String sql = String.format(SCRIPT_COPY_COLLECTION, newTableName, tableName); + String sql = String.format(SCRIPT_COPY_COLLECTION, MongodbSqlEscapes.requireMongoName(newTableName, "collection name"), + MongodbSqlEscapes.requireMongoName(tableName, "collection name")); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java index f1c37a565..8431a90f9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java @@ -82,7 +82,7 @@ public PageResult tables(Connection connection, String databaseName, Stri @Override public List columns(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_COLUMNS, tableName); + String sql = String.format(SELECT_TABLE_COLUMNS, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); List tableColumns = new ArrayList(); return (List) DefaultSQLExecutor.getInstance().execute(connection, sql, (resultSet) -> { while (resultSet.next()) { @@ -106,7 +106,7 @@ public List columns(Connection connection, String databaseName, String schemaNam } public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_INDEX, tableName); + String sql = String.format(SELECT_TABLE_INDEX, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); List tableIndexes = new ArrayList<>(); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { @@ -141,7 +141,7 @@ private void executeUse(String schemaName) { return; } Connection connection = Chat2DBContext.getConnection(); - String sql = String.format(SCRIPT_USE_SCHEMA, schemaName); + String sql = String.format(SCRIPT_USE_SCHEMA, MongodbSqlEscapes.requireMongoName(schemaName, "database name")); try { DefaultSQLExecutor.getInstance().execute(connection, sql); } catch (SQLException e) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java index 6582e8502..b52b7796a 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java @@ -62,7 +62,7 @@ public List executeSelectTable(SqlExecuteRequest command) { if (StringUtils.isEmpty(command.getTableName())) { return Collections.emptyList(); } - String sql = String.format(EXECUTE_SQL, command.getTableName()); + String sql = String.format(EXECUTE_SQL, MongodbSqlEscapes.requireMongoName(command.getTableName(), "collection name")); command.setScript(sql); return execute(command); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java index 4b0f5cf3b..aab370897 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java @@ -79,7 +79,8 @@ private String buildDeleteSql(String tableName, List deleteSqlCommands, if (StringUtils.isEmpty(idValue)) { return StringUtils.EMPTY; } - String sql = String.format(SQL_DB_DOT_FORMAT_DOT_DELETEONE_OPEN_PAREN_OPEN_BRACE, tableName, idValue); + String sql = String.format(SQL_DB_DOT_FORMAT_DOT_DELETEONE_OPEN_PAREN_OPEN_BRACE, + MongodbSqlEscapes.requireMongoName(tableName, "collection name"), MongodbSqlEscapes.escapeJsonString(idValue)); log.info(LOG_DELETE_SQL, sql); return sql; @@ -95,13 +96,13 @@ private String buildInsertSql(String tableName, List
headerList, ResultO for (int i = 2; i < newDataList.size(); i++) { Header header = headerList.get(i); String newValue = newDataList.get(i); - sql.append(header.getName()).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(newValue).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); + sql.append(MongodbSqlEscapes.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlEscapes.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); } if (sql.isEmpty()) { return StringUtils.EMPTY; } StringBuffer insertSql = new StringBuffer(); - insertSql.append(String.format(SQL_DB_DOT_FORMAT_DOT_INSERTONE, tableName)).append(SQLConstants.OPEN_PARENTHESIS) + insertSql.append(String.format(SQL_DB_DOT_FORMAT_DOT_INSERTONE, MongodbSqlEscapes.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.OPEN_CURLY_BRACE) .append(sql.deleteCharAt(sql.length() - 1)) .append(SQLConstants.CLOSE_CURLY_BRACE) @@ -127,20 +128,20 @@ private String buildUpdate(String tableName, List
headerList, ResultOper if (_idValue.isEmpty()) { _idValue.append(oldDataList.get(1)); } - setSql.append(header.getName()).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(newValue).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); + setSql.append(MongodbSqlEscapes.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlEscapes.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); } if (_idValue.isEmpty() || setSql.isEmpty()) { return StringUtils.EMPTY; } StringBuffer sql = new StringBuffer(); - sql.append(String.format(SQL_DB_DOT_FORMAT_DOT_UPDATEONE, tableName)).append(SQLConstants.OPEN_PARENTHESIS) + sql.append(String.format(SQL_DB_DOT_FORMAT_DOT_UPDATEONE, MongodbSqlEscapes.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.OPEN_CURLY_BRACE) .append(MONGODB_ID_FIELD) .append(SQLConstants.COLON) .append(MONGODB_OBJECT_ID_TYPE) .append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.DOUBLE_QUOTE) - .append(_idValue.toString()) + .append(MongodbSqlEscapes.escapeJsonString(_idValue.toString())) .append(SQLConstants.DOUBLE_QUOTE) .append(SQLConstants.CLOSE_PARENTHESIS) .append(SQLConstants.CLOSE_CURLY_BRACE) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java new file mode 100644 index 000000000..ee1df6c29 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java @@ -0,0 +1,71 @@ +package ai.chat2db.plugin.mongodb; + +import java.util.regex.Pattern; + +/** + * Neutralization helpers for values interpolated into Mongo shell command text (#1914). + * Shell commands are not plain SQL: database/collection names are validated against a strict + * allowlist, and string values inside JSON documents are JSON-escaped. + */ +public final class MongodbSqlEscapes { + + private static final Pattern MONGO_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_$-]+$"); + + private MongodbSqlEscapes() { + } + + /** + * Validate a database/collection/field name interpolated into shell command text such as + * {@code use } or {@code db..find()}. Rejects anything outside the allowlist. + */ + public static String requireMongoName(String name, String what) { + if (name == null || !MONGO_NAME_PATTERN.matcher(name).matches()) { + throw new IllegalArgumentException("Invalid MongoDB " + what + ": " + name); + } + return name; + } + + /** + * Escape a value interpolated into a double-quoted JSON string inside a shell command + * (surrounding quotes NOT added). + */ + public static String escapeJsonString(String value) { + if (value == null) { + return null; + } + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + default: + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } + } + return sb.toString(); + } +} diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java new file mode 100644 index 000000000..65c34dd00 --- /dev/null +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java @@ -0,0 +1,128 @@ +package ai.chat2db.plugin.mongodb; + +import java.util.List; + +import ai.chat2db.community.domain.api.model.result.Header; +import ai.chat2db.community.domain.api.model.result.QueryResponse; +import ai.chat2db.community.domain.api.model.result.ResultOperation; +import ai.chat2db.spi.constant.SQLConstants; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MongodbSqlEscapesTest { + + @Test + void requireMongoNameAcceptsLegitimateNames() { + assertEquals("mydb", MongodbSqlEscapes.requireMongoName("mydb", "database name")); + assertEquals("my_db-1$A", MongodbSqlEscapes.requireMongoName("my_db-1$A", "collection name")); + } + + @Test + void requireMongoNameRejectsInjection() { + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName("a.b", "collection name")); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName("a b", "collection name")); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName("x; db.dropDatabase(); //", "database name")); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName("x\")}", "collection name")); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName("", "collection name")); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlEscapes.requireMongoName(null, "collection name")); + } + + @Test + void escapeJsonStringEscapesQuotesBackslashAndControls() { + assertEquals("plain", MongodbSqlEscapes.escapeJsonString("plain")); + assertEquals("a\\\"b", MongodbSqlEscapes.escapeJsonString("a\"b")); + assertEquals("a\\\\b", MongodbSqlEscapes.escapeJsonString("a\\b")); + assertEquals("a\\nb", MongodbSqlEscapes.escapeJsonString("a\nb")); + assertEquals("\\u0001", MongodbSqlEscapes.escapeJsonString("\u0001")); + assertNull(MongodbSqlEscapes.escapeJsonString(null)); + } + + @Test + void dropTableRejectsMaliciousName() { + MongodbDBManager manager = new MongodbDBManager(); + assertEquals(" db. users.drop();", manager.dropTable(null, null, null, "users")); + assertThrows(IllegalArgumentException.class, + () -> manager.dropTable(null, null, null, "users; db.dropDatabase(); //")); + } + + @Test + void truncateTableRejectsMaliciousName() throws Exception { + MongodbDBManager manager = new MongodbDBManager(); + assertEquals("db.users.deleteMany({})", manager.truncateTable(null, null, null, "users")); + assertThrows(IllegalArgumentException.class, + () -> manager.truncateTable(null, null, null, "a.b")); + } + + @Test + void deleteCommandEscapesObjectIdAndValidatesTable() { + QueryResponse response = new QueryResponse(); + response.setTableName("users"); + response.setHeaderList(List.of( + Header.builder().name("rn").build(), + Header.builder().name("_id").build())); + ResultOperation operation = new ResultOperation(); + operation.setType(SQLConstants.DELETE_KEYWORD); + operation.setOldDataList(List.of("1", "abc123")); + response.setOperations(List.of(operation)); + assertEquals("db.users.deleteOne({_id: ObjectId(\"abc123\")})", + MongodbSqlBuilder.getInstance().buildByQueryResult(response)); + + operation.setOldDataList(List.of("1", "a\"), $where: 1, x: (\"")); + String sql = MongodbSqlBuilder.getInstance().buildByQueryResult(response); + assertEquals("db.users.deleteOne({_id: ObjectId(\"a\\\"), $where: 1, x: (\\\"\")})", sql); + + response.setTableName("users; //"); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlBuilder.getInstance().buildByQueryResult(response)); + } + + @Test + void insertCommandEscapesValuesAndValidatesNames() { + QueryResponse response = new QueryResponse(); + response.setTableName("users"); + response.setHeaderList(List.of( + Header.builder().name("rn").build(), + Header.builder().name("_id").build(), + Header.builder().name("name").build())); + ResultOperation operation = new ResultOperation(); + operation.setType(SQLConstants.CREATE_KEYWORD); + operation.setDataList(List.of("1", "id1", "a\"}), db.dropDatabase(), //")); + response.setOperations(List.of(operation)); + String sql = MongodbSqlBuilder.getInstance().buildByQueryResult(response); + assertEquals("db.users.insertOne({name:\"a\\\"}), db.dropDatabase(), //\"})", sql); + + // malicious field name is rejected + response.setHeaderList(List.of( + Header.builder().name("rn").build(), + Header.builder().name("_id").build(), + Header.builder().name("a}), x:(").build())); + assertThrows(IllegalArgumentException.class, + () -> MongodbSqlBuilder.getInstance().buildByQueryResult(response)); + } + + @Test + void updateCommandEscapesValuesAndId() { + QueryResponse response = new QueryResponse(); + response.setTableName("users"); + response.setHeaderList(List.of( + Header.builder().name("rn").build(), + Header.builder().name("_id").build(), + Header.builder().name("name").build())); + ResultOperation operation = new ResultOperation(); + operation.setType(SQLConstants.UPDATE_KEYWORD); + operation.setOldDataList(List.of("1", "id\"1", "old")); + operation.setDataList(List.of("1", "id\"1", "new\"value")); + response.setOperations(List.of(operation)); + String sql = MongodbSqlBuilder.getInstance().buildByQueryResult(response); + assertEquals("db.users.updateOne({_id:ObjectId(\"id\\\"1\")},{$set:{name:\"new\\\"value\"}})", sql); + } +} From c694cc435ffd7286549b93d225fe0c65c4dae8fc Mon Sep 17 00:00:00 2001 From: HandSonic <8078023+handsonic@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:05:06 +0800 Subject: [PATCH 2/2] refactor(mongodb): rename MongodbSqlEscapes to MongodbSqlGuards per maintainer review (#1914) - non-SQL module: no SQL identifier/string escaping, default ISQLIdentifierProcessor left untouched - requireMongoName allowlist + escapeJsonString are validation/JSON guards, not SQL escaping; class renamed accordingly - callers rewired (DBManager/MetaData/ScriptExecutor/SqlBuilder) - tests migrated to MongodbSqlGuardsTest (8 green) --- .../plugin/mongodb/MongodbDBManager.java | 10 +++---- .../plugin/mongodb/MongodbMetaData.java | 6 ++-- .../plugin/mongodb/MongodbScriptExecutor.java | 2 +- .../plugin/mongodb/MongodbSqlBuilder.java | 12 ++++---- ...bSqlEscapes.java => MongodbSqlGuards.java} | 10 +++---- ...pesTest.java => MongodbSqlGuardsTest.java} | 30 +++++++++---------- 6 files changed, 35 insertions(+), 35 deletions(-) rename chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/{MongodbSqlEscapes.java => MongodbSqlGuards.java} (85%) rename chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/{MongodbSqlEscapesTest.java => MongodbSqlGuardsTest.java} (80%) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java index 2c4d3dd64..533fdb075 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbDBManager.java @@ -29,7 +29,7 @@ public void connectDatabase(Connection connection, String database) { return; } try { - DefaultSQLExecutor.getInstance().execute(connection, String.format(SCRIPT_USE_SCHEMA, MongodbSqlEscapes.requireMongoName(schemaName, "database name"))); + DefaultSQLExecutor.getInstance().execute(connection, String.format(SCRIPT_USE_SCHEMA, MongodbSqlGuards.requireMongoName(schemaName, "database name"))); } catch (SQLException e) { throw new RuntimeException(e); } @@ -37,18 +37,18 @@ public void connectDatabase(Connection connection, String database) { @Override public String dropTable(Connection connection, String databaseName, String schemaName, String tableName) { - return String.format(SCRIPT_DROP_COLLECTION, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); + return String.format(SCRIPT_DROP_COLLECTION, MongodbSqlGuards.requireMongoName(tableName, "collection name")); } @Override public String truncateTable(Connection connection, String databaseName, String schemaName, String tableName) throws SQLException { - return String.format(SCRIPT_TRUNCATE_COLLECTION, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); + return String.format(SCRIPT_TRUNCATE_COLLECTION, MongodbSqlGuards.requireMongoName(tableName, "collection name")); } @Override public void copyTable(Connection connection, String databaseName, String schemaName, String tableName, String newTableName,boolean copyData) throws SQLException { - String sql = String.format(SCRIPT_COPY_COLLECTION, MongodbSqlEscapes.requireMongoName(newTableName, "collection name"), - MongodbSqlEscapes.requireMongoName(tableName, "collection name")); + String sql = String.format(SCRIPT_COPY_COLLECTION, MongodbSqlGuards.requireMongoName(newTableName, "collection name"), + MongodbSqlGuards.requireMongoName(tableName, "collection name")); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> null); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java index 8431a90f9..ab7f7190e 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbMetaData.java @@ -82,7 +82,7 @@ public PageResult
tables(Connection connection, String databaseName, Stri @Override public List columns(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_COLUMNS, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); + String sql = String.format(SELECT_TABLE_COLUMNS, MongodbSqlGuards.requireMongoName(tableName, "collection name")); List tableColumns = new ArrayList(); return (List) DefaultSQLExecutor.getInstance().execute(connection, sql, (resultSet) -> { while (resultSet.next()) { @@ -106,7 +106,7 @@ public List columns(Connection connection, String databaseName, String schemaNam } public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { - String sql = String.format(SELECT_TABLE_INDEX, MongodbSqlEscapes.requireMongoName(tableName, "collection name")); + String sql = String.format(SELECT_TABLE_INDEX, MongodbSqlGuards.requireMongoName(tableName, "collection name")); List tableIndexes = new ArrayList<>(); DefaultSQLExecutor.getInstance().execute(connection, sql, resultSet -> { while (resultSet.next()) { @@ -141,7 +141,7 @@ private void executeUse(String schemaName) { return; } Connection connection = Chat2DBContext.getConnection(); - String sql = String.format(SCRIPT_USE_SCHEMA, MongodbSqlEscapes.requireMongoName(schemaName, "database name")); + String sql = String.format(SCRIPT_USE_SCHEMA, MongodbSqlGuards.requireMongoName(schemaName, "database name")); try { DefaultSQLExecutor.getInstance().execute(connection, sql); } catch (SQLException e) { diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java index b52b7796a..5cc2a5258 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbScriptExecutor.java @@ -62,7 +62,7 @@ public List executeSelectTable(SqlExecuteRequest command) { if (StringUtils.isEmpty(command.getTableName())) { return Collections.emptyList(); } - String sql = String.format(EXECUTE_SQL, MongodbSqlEscapes.requireMongoName(command.getTableName(), "collection name")); + String sql = String.format(EXECUTE_SQL, MongodbSqlGuards.requireMongoName(command.getTableName(), "collection name")); command.setScript(sql); return execute(command); } diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java index aab370897..dc5c2f075 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlBuilder.java @@ -80,7 +80,7 @@ private String buildDeleteSql(String tableName, List deleteSqlCommands, return StringUtils.EMPTY; } String sql = String.format(SQL_DB_DOT_FORMAT_DOT_DELETEONE_OPEN_PAREN_OPEN_BRACE, - MongodbSqlEscapes.requireMongoName(tableName, "collection name"), MongodbSqlEscapes.escapeJsonString(idValue)); + MongodbSqlGuards.requireMongoName(tableName, "collection name"), MongodbSqlGuards.escapeJsonString(idValue)); log.info(LOG_DELETE_SQL, sql); return sql; @@ -96,13 +96,13 @@ private String buildInsertSql(String tableName, List
headerList, ResultO for (int i = 2; i < newDataList.size(); i++) { Header header = headerList.get(i); String newValue = newDataList.get(i); - sql.append(MongodbSqlEscapes.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlEscapes.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); + sql.append(MongodbSqlGuards.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlGuards.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); } if (sql.isEmpty()) { return StringUtils.EMPTY; } StringBuffer insertSql = new StringBuffer(); - insertSql.append(String.format(SQL_DB_DOT_FORMAT_DOT_INSERTONE, MongodbSqlEscapes.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) + insertSql.append(String.format(SQL_DB_DOT_FORMAT_DOT_INSERTONE, MongodbSqlGuards.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.OPEN_CURLY_BRACE) .append(sql.deleteCharAt(sql.length() - 1)) .append(SQLConstants.CLOSE_CURLY_BRACE) @@ -128,20 +128,20 @@ private String buildUpdate(String tableName, List
headerList, ResultOper if (_idValue.isEmpty()) { _idValue.append(oldDataList.get(1)); } - setSql.append(MongodbSqlEscapes.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlEscapes.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); + setSql.append(MongodbSqlGuards.requireMongoName(header.getName(), "field name")).append(SQLConstants.COLON).append(SQLConstants.DOUBLE_QUOTE).append(MongodbSqlGuards.escapeJsonString(newValue)).append(SQLConstants.DOUBLE_QUOTE).append(SQLConstants.COMMA); } if (_idValue.isEmpty() || setSql.isEmpty()) { return StringUtils.EMPTY; } StringBuffer sql = new StringBuffer(); - sql.append(String.format(SQL_DB_DOT_FORMAT_DOT_UPDATEONE, MongodbSqlEscapes.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) + sql.append(String.format(SQL_DB_DOT_FORMAT_DOT_UPDATEONE, MongodbSqlGuards.requireMongoName(tableName, "collection name"))).append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.OPEN_CURLY_BRACE) .append(MONGODB_ID_FIELD) .append(SQLConstants.COLON) .append(MONGODB_OBJECT_ID_TYPE) .append(SQLConstants.OPEN_PARENTHESIS) .append(SQLConstants.DOUBLE_QUOTE) - .append(MongodbSqlEscapes.escapeJsonString(_idValue.toString())) + .append(MongodbSqlGuards.escapeJsonString(_idValue.toString())) .append(SQLConstants.DOUBLE_QUOTE) .append(SQLConstants.CLOSE_PARENTHESIS) .append(SQLConstants.CLOSE_CURLY_BRACE) diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlGuards.java similarity index 85% rename from chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java rename to chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlGuards.java index ee1df6c29..398519e33 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapes.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/main/java/ai/chat2db/plugin/mongodb/MongodbSqlGuards.java @@ -3,15 +3,15 @@ import java.util.regex.Pattern; /** - * Neutralization helpers for values interpolated into Mongo shell command text (#1914). - * Shell commands are not plain SQL: database/collection names are validated against a strict - * allowlist, and string values inside JSON documents are JSON-escaped. + * Validation and JSON-escaping guards for values interpolated into Mongo shell command + * text (#1914). Shell commands are not plain SQL: database/collection names are validated + * against a strict allowlist, and string values inside JSON documents are JSON-escaped. */ -public final class MongodbSqlEscapes { +public final class MongodbSqlGuards { private static final Pattern MONGO_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_$-]+$"); - private MongodbSqlEscapes() { + private MongodbSqlGuards() { } /** diff --git a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlGuardsTest.java similarity index 80% rename from chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java rename to chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlGuardsTest.java index 65c34dd00..c4e9939d9 100644 --- a/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlEscapesTest.java +++ b/chat2db-community-server/chat2db-community-plugins/chat2db-community-mongodb/src/test/java/ai/chat2db/plugin/mongodb/MongodbSqlGuardsTest.java @@ -12,38 +12,38 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -class MongodbSqlEscapesTest { +class MongodbSqlGuardsTest { @Test void requireMongoNameAcceptsLegitimateNames() { - assertEquals("mydb", MongodbSqlEscapes.requireMongoName("mydb", "database name")); - assertEquals("my_db-1$A", MongodbSqlEscapes.requireMongoName("my_db-1$A", "collection name")); + assertEquals("mydb", MongodbSqlGuards.requireMongoName("mydb", "database name")); + assertEquals("my_db-1$A", MongodbSqlGuards.requireMongoName("my_db-1$A", "collection name")); } @Test void requireMongoNameRejectsInjection() { assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName("a.b", "collection name")); + () -> MongodbSqlGuards.requireMongoName("a.b", "collection name")); assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName("a b", "collection name")); + () -> MongodbSqlGuards.requireMongoName("a b", "collection name")); assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName("x; db.dropDatabase(); //", "database name")); + () -> MongodbSqlGuards.requireMongoName("x; db.dropDatabase(); //", "database name")); assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName("x\")}", "collection name")); + () -> MongodbSqlGuards.requireMongoName("x\")}", "collection name")); assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName("", "collection name")); + () -> MongodbSqlGuards.requireMongoName("", "collection name")); assertThrows(IllegalArgumentException.class, - () -> MongodbSqlEscapes.requireMongoName(null, "collection name")); + () -> MongodbSqlGuards.requireMongoName(null, "collection name")); } @Test void escapeJsonStringEscapesQuotesBackslashAndControls() { - assertEquals("plain", MongodbSqlEscapes.escapeJsonString("plain")); - assertEquals("a\\\"b", MongodbSqlEscapes.escapeJsonString("a\"b")); - assertEquals("a\\\\b", MongodbSqlEscapes.escapeJsonString("a\\b")); - assertEquals("a\\nb", MongodbSqlEscapes.escapeJsonString("a\nb")); - assertEquals("\\u0001", MongodbSqlEscapes.escapeJsonString("\u0001")); - assertNull(MongodbSqlEscapes.escapeJsonString(null)); + assertEquals("plain", MongodbSqlGuards.escapeJsonString("plain")); + assertEquals("a\\\"b", MongodbSqlGuards.escapeJsonString("a\"b")); + assertEquals("a\\\\b", MongodbSqlGuards.escapeJsonString("a\\b")); + assertEquals("a\\nb", MongodbSqlGuards.escapeJsonString("a\nb")); + assertEquals("\\u0001", MongodbSqlGuards.escapeJsonString("\u0001")); + assertNull(MongodbSqlGuards.escapeJsonString(null)); } @Test