Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.apache.fineract.infrastructure.dataqueries.domain.ReportType;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.report.service.ReportParameterTypeResolver;
import org.apache.fineract.infrastructure.security.exception.InputValidationException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.security.service.SqlInjectionPreventerService;
import org.apache.fineract.infrastructure.security.utils.LogParameterEscapeUtil;
Expand Down Expand Up @@ -164,8 +165,8 @@ private Object castParamValue(String value, String formatType) {
return value;
}

private PreparedQuery buildPreparedQuery(final String name, final Map<String, String> queryParams, final String sql) {
final Map<String, String> paramFormatTypes = this.reportParameterTypeResolver.loadParamFormatTypes(name);
private PreparedQuery buildPreparedQuery(final String name, final Map<String, String> queryParams, final String sql,
final Map<String, String> paramFormatTypes) {
final List<Object> paramValues = new ArrayList<>();
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(sql);
final StringBuilder preparedSql = new StringBuilder();
Expand Down Expand Up @@ -205,6 +206,7 @@ private PreparedQuery buildPreparedQuery(final String name, final Map<String, St
*/
private PreparedQuery getSQLtoRun(final String name, final String type, final Map<String, String> queryParams) {

final Map<String, String> paramFormatTypes = this.reportParameterTypeResolver.loadParamFormatTypes(name);
String sql = getSql(name, type);

// Step 1 — resolve server-controlled placeholders as plain strings (not user input)
Expand All @@ -221,14 +223,20 @@ private PreparedQuery getSQLtoRun(final String name, final String type, final Ma
sql = Pattern.compile(Pattern.quote("CURRENT_DATE"), Pattern.CASE_INSENSITIVE).matcher(sql)
.replaceAll(Matcher.quoteReplacement(sqlGenerator.currentBusinessDate()));

// Step 2.5a — resolve display-literal placeholders: '${param}' AS alias
// These are not filter values so direct substitution is safe.
// Step 2.5a — display-literal substitution for date/number params only
// Substitute as plain string to preserve varchar return type expected by callers
for (Map.Entry<String, String> entry : queryParams.entrySet()) {
if (entry.getKey().startsWith("${")) {
String paramName = entry.getKey().substring(2, entry.getKey().length() - 1);
// Replace display-literal pattern: '${param}' followed by AS
sql = sql.replaceAll("'" + Pattern.quote("${" + paramName + "}") + "'(\\s+AS\\s+)",
"'" + Matcher.quoteReplacement(entry.getValue()) + "'$1");
String paramName = entry.getKey().startsWith("${") ? entry.getKey().substring(2, entry.getKey().length() - 1) : entry.getKey();
String formatType = paramFormatTypes.get(paramName);
String displayPattern = "'\\$\\{" + Pattern.quote(paramName) + "\\}'(\\s+AS\\s+)";
if (sql.matches("(?s).*" + displayPattern + ".*")) {
if (formatType == null || (!formatType.equalsIgnoreCase("number") && !formatType.equalsIgnoreCase("integer")
&& !formatType.equalsIgnoreCase("date"))) {
throw new InputValidationException("Parameter '%s' of type '%s' cannot be used in display-literal position"
.formatted(paramName, formatType != null ? formatType : "unregistered"));
}
// Substitute as string literal — preserves varchar return type
sql = sql.replaceAll(displayPattern, "'" + Matcher.quoteReplacement(entry.getValue()) + "'$1");
}
}

Expand All @@ -247,7 +255,7 @@ private PreparedQuery getSQLtoRun(final String name, final String type, final Ma
normalisedParams.put(key, entry.getValue());
}

return buildPreparedQuery(name, normalisedParams, sql);
return buildPreparedQuery(name, normalisedParams, sql, paramFormatTypes);
}

private String getSql(final String name, final String type) {
Expand Down Expand Up @@ -592,13 +600,14 @@ public GenericResultsetData retrieveGenericResultSetForSmsEmailCampaign(String n
* are bound as {@code ?} variables — never concatenated into the SQL string.
*/
private PreparedQuery sqlToRunForSmsEmailCampaign(final String name, final String type, final Map<String, String> queryParams) {
final Map<String, String> paramFormatTypes = this.reportParameterTypeResolver.loadParamFormatTypes(name);
String sql = getSql(name, type);

sql = sql.replaceAll("'(\\$\\{[^}]+\\})'", "$1");
sql = sql.replaceAll("\"(\\$\\{[^}]+\\})\"", "$1");
sql = sql.replaceAll("\"(-?\\d+)\"", "$1");

return buildPreparedQuery(name, queryParams, sql);
return buildPreparedQuery(name, queryParams, sql, paramFormatTypes);
}

@Override
Expand Down