From 01b8bc028431d3e686214511bfb3a20771dacc48 Mon Sep 17 00:00:00 2001 From: Terence Monteiro Date: Tue, 16 Jun 2026 10:05:33 +0530 Subject: [PATCH] FINERACT-2642: string param AS header interpolation --- .../service/ReadReportingServiceImpl.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java index 3e2154aa17c..a1bd759497a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadReportingServiceImpl.java @@ -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; @@ -164,8 +165,8 @@ private Object castParamValue(String value, String formatType) { return value; } - private PreparedQuery buildPreparedQuery(final String name, final Map queryParams, final String sql) { - final Map paramFormatTypes = this.reportParameterTypeResolver.loadParamFormatTypes(name); + private PreparedQuery buildPreparedQuery(final String name, final Map queryParams, final String sql, + final Map paramFormatTypes) { final List paramValues = new ArrayList<>(); final Matcher matcher = PLACEHOLDER_PATTERN.matcher(sql); final StringBuilder preparedSql = new StringBuilder(); @@ -205,6 +206,7 @@ private PreparedQuery buildPreparedQuery(final String name, final Map queryParams) { + final Map paramFormatTypes = this.reportParameterTypeResolver.loadParamFormatTypes(name); String sql = getSql(name, type); // Step 1 — resolve server-controlled placeholders as plain strings (not user input) @@ -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 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"); } } @@ -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) { @@ -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 queryParams) { + final Map 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