FINERACT-2081: ESAPI.encoder().encodeForSQL is deprecated, so replaced the method with parameterized queries based solution with user input validation

This commit is contained in:
Attila Budai
2025-09-01 13:41:49 +02:00
committed by Adam Saghy
parent 917dd4d117
commit 567e1c9800
7 changed files with 827 additions and 23 deletions
@@ -0,0 +1,88 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.dataqueries.domain;
import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Enumeration of valid report types for whitelist validation.
*
* This enum provides a secure whitelist of allowed report types to prevent SQL injection attacks in the reporting
* system. Only these predefined types are allowed in report queries.
*/
public enum ReportType {
/**
* Standard report type for retrieving report data
*/
REPORT("report"),
/**
* Parameter type for retrieving parameter definitions and possible values
*/
PARAMETER("parameter");
private final String value;
/**
* Cached set of all valid values for efficient validation
*/
private static final Set<String> VALID_VALUES = Arrays.stream(values()).map(ReportType::getValue).collect(Collectors.toSet());
ReportType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
/**
* Validates if a given report type is in the whitelist.
*
* @param type
* the report type to validate
* @return true if the type is valid, false otherwise
*/
public static boolean isValidType(String type) {
return type != null && !type.trim().isEmpty() && VALID_VALUES.contains(type.toLowerCase(Locale.ROOT));
}
/**
* Gets the ReportType enum value for a given string.
*
* @param type
* the report type string
* @return the corresponding ReportType enum value
* @throws IllegalArgumentException
* if the type is not valid
*/
public static ReportType fromValue(String type) {
if (type == null) {
throw new IllegalArgumentException("Report type cannot be null");
}
String lowerType = type.toLowerCase(Locale.ROOT);
return Arrays.stream(values()).filter(rt -> rt.getValue().equals(lowerType)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid report type: " + type));
}
}
@@ -21,4 +21,29 @@ package org.apache.fineract.infrastructure.security.service;
public interface SqlInjectionPreventerService {
String encodeSql(String literal);
/**
* Validates and quotes a database identifier (table name, column name) using database-specific quoting rules. This
* method ensures that identifiers are safely quoted to prevent SQL injection attacks.
*
* @param identifier
* the database identifier to quote
* @param allowedValues
* optional set of allowed values for whitelist validation
* @return the properly quoted identifier safe for use in SQL queries
* @throws IllegalArgumentException
* if the identifier is invalid or not in the whitelist (if provided)
*/
String quoteIdentifier(String identifier, java.util.Set<String> allowedValues);
/**
* Validates and quotes a database identifier without whitelist validation.
*
* @param identifier
* the database identifier to quote
* @return the properly quoted identifier safe for use in SQL queries
* @throws IllegalArgumentException
* if the identifier is null or empty
*/
String quoteIdentifier(String identifier);
}
@@ -34,6 +34,7 @@ import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.security.service.SqlValidator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
@@ -59,16 +60,19 @@ public class ColumnValidator {
ResultSet resultSet = dbMetaData.getColumns(null, null, entry.getKey(), null);
Set<String> tableColumns = getTableColumns(resultSet);
if (!columns.isEmpty() && tableColumns.isEmpty()) {
throw new SQLInjectionException();
throw new PlatformApiDataValidationException("error.msg.invalid.table.column", "Invalid table or column name detected",
entry.getKey(), columns);
}
for (String requestedColumn : columns) {
if (!tableColumns.contains(requestedColumn)) {
throw new SQLInjectionException();
throw new PlatformApiDataValidationException("error.msg.invalid.table.column", "Invalid table column name detected",
entry.getKey(), requestedColumn);
}
}
}
} catch (SQLException e) {
throw new SQLInjectionException(e);
throw new PlatformApiDataValidationException("error.msg.database.access.error",
"Database access error during column validation", e.getMessage(), e);
} finally {
if (connection != null) {
DataSourceUtils.releaseConnection(connection, jdbcTemplate.getDataSource());
@@ -120,7 +124,8 @@ public class ColumnValidator {
Set<String> columns = entry.getValue();
tableColumnMap.put(schema.substring(startPos, index).trim(), columns);
} else {
throw new SQLInjectionException();
throw new PlatformApiDataValidationException("error.msg.invalid.table.alias", "Invalid table alias in SQL query",
entry.getKey());
}
}
@@ -142,7 +147,8 @@ public class ColumnValidator {
tableColumnMap.put(tableColumn[0], columns);
}
} else {
throw new SQLInjectionException();
throw new PlatformApiDataValidationException("error.msg.invalid.table.column.format",
"Invalid table.column format in operand", operand);
}
}
return tableColumnMap;