@@ -80,16 +80,7 @@ public ResponseEntity<?> executeQuery(
8080 return ResponseEntity .status (HttpStatus .SC_UNAUTHORIZED ).body ("User not authenticated" );
8181 }
8282
83- List <IntegrationSecurityToken > databaseIntegrations = integrationSecurityTokenService
84- .findByConnectionType ("database" );
85-
86- if (databaseIntegrations .isEmpty ()) {
87- return ResponseEntity .status (HttpStatus .SC_NOT_FOUND ).body ("No database integration configured" );
88- }
89-
90- IntegrationSecurityToken databaseIntegration = databaseIntegrations .get (0 );
91- ExternalIntegrationDTO integrationDTO = new ExternalIntegrationDTO (databaseIntegration , true );
92-
83+ // Validate query input first before checking database integration
9384 String query = (String ) queryPayload .get ("query" );
9485 if (query == null || query .trim ().isEmpty ()) {
9586 return ResponseEntity .badRequest ().body (Map .of ("error" , "Query parameter is required" ));
@@ -100,39 +91,54 @@ public ResponseEntity<?> executeQuery(
10091 .body (Map .of ("error" , "Only SELECT queries are allowed" ));
10192 }
10293
103- if (!isSafeSelectQuery (query )) {
104- return ResponseEntity .status (HttpStatus .SC_FORBIDDEN )
105- .body (Map .of ("error" , "Query is not allowed" ));
94+ List <IntegrationSecurityToken > databaseIntegrations = integrationSecurityTokenService
95+ .findByConnectionType ("database" );
96+
97+ if (databaseIntegrations .isEmpty ()) {
98+ return ResponseEntity .status (HttpStatus .SC_NOT_FOUND ).body ("No database integration configured" );
10699 }
107100
101+ IntegrationSecurityToken databaseIntegration = databaseIntegrations .get (0 );
102+ ExternalIntegrationDTO integrationDTO = new ExternalIntegrationDTO (databaseIntegration , true );
103+
108104 String jdbcUrl = buildJdbcUrl (integrationDTO );
109105 List <Map <String , Object >> results = new ArrayList <>();
110106
107+ // Set a maximum row limit to prevent resource exhaustion
108+ final int MAX_ROWS = 1000 ;
109+
111110 try (Connection conn = DriverManager .getConnection (
112111 jdbcUrl ,
113112 integrationDTO .getUsername (),
114113 integrationDTO .getApiToken ()
115- );
116- Statement stmt = conn .createStatement ();
117- ResultSet rs = stmt .executeQuery (query )) {
118-
119- ResultSetMetaData metaData = rs .getMetaData ();
120- int columnCount = metaData .getColumnCount ();
121-
122- while (rs .next ()) {
123- Map <String , Object > row = new LinkedHashMap <>();
124- for (int i = 1 ; i <= columnCount ; i ++) {
125- row .put (metaData .getColumnName (i ), rs .getObject (i ));
114+ )) {
115+ // Set connection to read-only mode to prevent any data modification
116+ conn .setReadOnly (true );
117+
118+ try (Statement stmt = conn .createStatement ()) {
119+ // Set max rows to prevent excessive memory usage
120+ stmt .setMaxRows (MAX_ROWS );
121+
122+ try (ResultSet rs = stmt .executeQuery (query )) {
123+ ResultSetMetaData metaData = rs .getMetaData ();
124+ int columnCount = metaData .getColumnCount ();
125+
126+ while (rs .next ()) {
127+ Map <String , Object > row = new LinkedHashMap <>();
128+ for (int i = 1 ; i <= columnCount ; i ++) {
129+ row .put (metaData .getColumnName (i ), rs .getObject (i ));
130+ }
131+ results .add (row );
132+ }
133+
134+ return ResponseEntity .ok (Map .of (
135+ "success" , true ,
136+ "rowCount" , results .size (),
137+ "data" , results ,
138+ "maxRows" , MAX_ROWS
139+ ));
126140 }
127- results .add (row );
128141 }
129-
130- return ResponseEntity .ok (Map .of (
131- "success" , true ,
132- "rowCount" , results .size (),
133- "data" , results
134- ));
135-
136142 } catch (SQLException e ) {
137143 log .error ("Database query failed" , e );
138144 return ResponseEntity .status (HttpStatus .SC_INTERNAL_SERVER_ERROR )
@@ -148,48 +154,6 @@ public ResponseEntity<?> executeQuery(
148154 }
149155 }
150156
151- /**
152- * Performs basic validation to ensure that only reasonably safe SELECT queries are executed.
153- * This method is intentionally conservative and will reject queries containing
154- * multiple statements, comments, or obvious DDL/DML keywords.
155- */
156- private boolean isSafeSelectQuery (String query ) {
157- if (query == null ) {
158- return false ;
159- }
160-
161- String trimmed = query .trim ();
162- if (trimmed .isEmpty ()) {
163- return false ;
164- }
165-
166- // Must start with SELECT (case-insensitive)
167- String upper = trimmed .toUpperCase (Locale .ROOT );
168- if (!upper .startsWith ("SELECT" )) {
169- return false ;
170- }
171-
172- // Disallow multiple statements and common comment syntaxes
173- if (upper .contains (";" ) || upper .contains ("--" ) || upper .contains ("/*" ) || upper .contains ("*/" ) || upper .contains ("#" )) {
174- return false ;
175- }
176-
177- // Disallow obviously dangerous keywords that should not appear in a simple read-only query
178- String [] forbiddenKeywords = {
179- " INSERT " , " UPDATE " , " DELETE " , " DROP " , " ALTER " , " TRUNCATE " ,
180- " CREATE " , " MERGE " , " GRANT " , " REVOKE " , " EXEC " , " EXECUTE " ,
181- " INTO OUTFILE " , " INTO DUMPFILE "
182- };
183-
184- for (String keyword : forbiddenKeywords ) {
185- if (upper .contains (keyword )) {
186- return false ;
187- }
188- }
189-
190- return true ;
191- }
192-
193157 @ GetMapping ("/tables" )
194158 @ Endpoint (description = "List database tables" )
195159 @ LimitAccess (applicationAccess = {ApplicationAccessEnum .CAN_LOG_IN })
@@ -282,6 +246,13 @@ public ResponseEntity<?> getTableSchema(
282246 return ResponseEntity .status (HttpStatus .SC_UNAUTHORIZED ).body ("User not authenticated" );
283247 }
284248
249+ // Validate table name to prevent SQL injection
250+ if (!isValidTableName (tableName )) {
251+ log .warn ("Invalid table name format: {}" , tableName );
252+ return ResponseEntity .badRequest ()
253+ .body (Map .of ("error" , "Invalid table name format. Only alphanumeric characters, underscores, and dots are allowed." ));
254+ }
255+
285256 List <IntegrationSecurityToken > databaseIntegrations = integrationSecurityTokenService
286257 .findByConnectionType ("database" );
287258
@@ -350,6 +321,42 @@ private String buildJdbcUrl(ExternalIntegrationDTO integrationDTO) {
350321
351322 private boolean isSelectQuery (String query ) {
352323 String trimmedQuery = query .trim ().toLowerCase ();
353- return trimmedQuery .startsWith ("select" );
324+
325+ // Remove SQL comments to prevent bypass attempts
326+ String cleanedQuery = trimmedQuery
327+ .replaceAll ("--[^\r \n ]*" , "" ) // Remove single-line comments (handles both \n and \r\n)
328+ .replaceAll ("(?s)/\\ *.*?\\ */" , "" ) // Remove multi-line comments (DOTALL flag for newlines)
329+ .replaceAll ("\\ s+" , " " ) // Normalize whitespace
330+ .trim ();
331+
332+ // Check if query starts with SELECT
333+ if (!cleanedQuery .startsWith ("select" )) {
334+ return false ;
335+ }
336+
337+ // Block dangerous keywords that could be used for SQL injection
338+ String [] dangerousKeywords = {
339+ ";" , "exec" , "execute" , "drop" , "delete" , "insert" , "update" ,
340+ "create" , "alter" , "truncate" , "grant" , "revoke" , "xp_"
341+ };
342+
343+ for (String keyword : dangerousKeywords ) {
344+ if (cleanedQuery .contains (keyword )) {
345+ log .warn ("Blocked query containing dangerous keyword: {}" , keyword );
346+ return false ;
347+ }
348+ }
349+
350+ return true ;
351+ }
352+
353+ private boolean isValidTableName (String tableName ) {
354+ if (tableName == null || tableName .trim ().isEmpty ()) {
355+ return false ;
356+ }
357+
358+ // Allow only alphanumeric characters, underscores, and dots (for schema.table format)
359+ // This prevents SQL injection via table name parameter
360+ return tableName .matches ("^[a-zA-Z0-9_.]+$" );
354361 }
355362}
0 commit comments