From 9197c3a818d5c1a496e61821ce8c5e4aadb10ba8 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 22 Jun 2026 15:54:24 -0600 Subject: [PATCH 1/2] Fix stored XSS in finance notification charge summary report FinanceNotification.createChargeSummaryReport is a divergent copy of the ehr_billing BillingNotification report and interpolated editor-entered database values (investigator, project, debitedAccount, projectNumber, category) and their derived URLs directly into HTML without escaping. That HTML is rendered verbatim into the LDK RunNotificationAction admin preview via HtmlString.unsafe and is also sent as the HTML email body, so a stored payload in any of those project/alias fields executed in the browser of any user who previewed the notification or received the email. Wrap every tainted value with PageFlowUtil.filter in both the per-financial-analyst tables and the top category summary table (which was also unescaped in this copy), matching the fix applied to BillingNotification. --- .../notification/FinanceNotification.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/onprc_billing/src/org/labkey/onprc_billing/notification/FinanceNotification.java b/onprc_billing/src/org/labkey/onprc_billing/notification/FinanceNotification.java index 3ebd9db8a..090fbcc9a 100644 --- a/onprc_billing/src/org/labkey/onprc_billing/notification/FinanceNotification.java +++ b/onprc_billing/src/org/labkey/onprc_billing/notification/FinanceNotification.java @@ -39,6 +39,7 @@ import org.labkey.api.query.QueryService; import org.labkey.api.query.UserSchema; import org.labkey.api.security.User; +import org.labkey.api.util.PageFlowUtil; import org.labkey.onprc_billing.ONPRC_BillingManager; import org.labkey.onprc_billing.ONPRC_BillingModule; import org.labkey.onprc_billing.ONPRC_BillingSchema; @@ -429,7 +430,7 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca Container container = containerMap.get(category); String url = getExecuteQueryUrl(container, ONPRC_BillingSchema.NAME, categoryToQuery.get(category), null) + "&query.param.StartDate=" + getDateFormat(c).format(start.getTime()) + "&query.param.EndDate=" + getDateFormat(c).format(endDate.getTime()); - msg.append("" + category + "" + totalsMap.get("total") + "" + _dollarFormat.format(totalsMap.get("totalCost")) + ""); + msg.append("" + PageFlowUtil.filter(category) + "" + totalsMap.get("total") + "" + _dollarFormat.format(totalsMap.get("totalCost")) + ""); } msg.append("

"); @@ -474,8 +475,8 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca String baseUrl = getExecuteQueryUrl(containerMap.get(category), ONPRC_BillingSchema.NAME, categoryToQuery.get(category), null) + "&query.param.StartDate=" + getDateFormat(c).format(start.getTime()) + "&query.param.EndDate=" + getDateFormat(c).format(endDate.getTime()); String projUrl = baseUrl + ("None".equals(tokens[1]) ? "&query.project/displayName~isblank" : "&query.project/displayName~eq=" + tokens[1]); - msg.append("" + financialAnalyst + ""); //the FA - msg.append("" + tokens[1] + ""); + msg.append("" + PageFlowUtil.filter(financialAnalyst) + ""); //the FA + msg.append("" + PageFlowUtil.filter(tokens[1]) + ""); //alias String accountUrl = null; @@ -487,22 +488,22 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca if (accountUrl != null) { - msg.append("" + tokens[2] + ""); + msg.append("" + PageFlowUtil.filter(tokens[2]) + ""); } else { - msg.append("" + (tokens[2]) + ""); + msg.append("" + PageFlowUtil.filter(tokens[2]) + ""); } - msg.append("" + (tokens[3]) + ""); - msg.append("" + category + ""); + msg.append("" + PageFlowUtil.filter(tokens[3]) + ""); + msg.append("" + PageFlowUtil.filter(category) + ""); for (FieldDescriptor fd : foundCols) { if (totals.containsKey(fd.getFieldName())) { String url = projUrl + fd.getFilter(); - msg.append("" + totals.get(fd.getFieldName()) + ""); + msg.append("" + totals.get(fd.getFieldName()) + ""); } else { From 4d2480c5d85c7c5e515e251a209a8ef3f8adae23 Mon Sep 17 00:00:00 2001 From: Marty Pradere Date: Mon, 22 Jun 2026 16:04:38 -0600 Subject: [PATCH 2/2] Fix stored XSS in DCM finance notification charge summary report DCMFinanceNotification overrides FinanceNotification.writeResultTable with its own copy of the report, so the escaping fix to the parent class did not cover it. It concatenated editor-entered database values (project, alias/account, OGA project number, category) and their derived URLs directly into HTML without escaping, reaching the same LDK RunNotificationAction admin preview (HtmlString.unsafe) and HTML email sink. It is registered as an active notification in ONPRC_BillingModule. Wrap every tainted value with PageFlowUtil.filter in both the per-project tables and the top category summary table, mapping to this override's token layout (tokens[0] project, tokens[1] alias, tokens[2] OGA project number). --- .../notification/DCMFinanceNotification.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/onprc_billing/src/org/labkey/onprc_billing/notification/DCMFinanceNotification.java b/onprc_billing/src/org/labkey/onprc_billing/notification/DCMFinanceNotification.java index 2da58ca5c..4b70e2d1b 100644 --- a/onprc_billing/src/org/labkey/onprc_billing/notification/DCMFinanceNotification.java +++ b/onprc_billing/src/org/labkey/onprc_billing/notification/DCMFinanceNotification.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.StringUtils; import org.labkey.api.data.Container; +import org.labkey.api.util.PageFlowUtil; import org.labkey.onprc_billing.ONPRC_BillingManager; import org.labkey.onprc_billing.ONPRC_BillingSchema; @@ -57,7 +58,7 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca Container container = containerMap.get(category); String url = getExecuteQueryUrl(container, ONPRC_BillingSchema.NAME, categoryToQuery.get(category), null) + "&query.param.StartDate=" + getDateFormat(c).format(start.getTime()) + "&query.param.EndDate=" + getDateFormat(c).format(endDate.getTime()); - msg.append("" + category + "" + totalsMap.get("total") + "" + _dollarFormat.format(totalsMap.get("totalCost")) + ""); + msg.append("" + PageFlowUtil.filter(category) + "" + totalsMap.get("total") + "" + _dollarFormat.format(totalsMap.get("totalCost")) + ""); } msg.append("

"); @@ -141,7 +142,7 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca String baseUrl = getExecuteQueryUrl(containerMap.get(category), ONPRC_BillingSchema.NAME, categoryToQuery.get(category), null) + "&query.param.StartDate=" + getDateFormat(c).format(start.getTime()) + "&query.param.EndDate=" + getDateFormat(c).format(endDate.getTime()); String projUrl = baseUrl + ("None".equals(tokens[0]) ? "&query.project/displayName~isblank" : "&query.project/displayName~eq=" + tokens[0]); - msg.append("" + tokens[0] + ""); + msg.append("" + PageFlowUtil.filter(tokens[0]) + ""); //alias String accountUrl = null; @@ -153,22 +154,22 @@ protected void writeResultTable(final StringBuilder msg, Date lastInvoiceEnd, Ca if (accountUrl != null) { - msg.append("" + tokens[1] + ""); + msg.append("" + PageFlowUtil.filter(tokens[1]) + ""); } else { - msg.append("" + (tokens[1]) + ""); + msg.append("" + PageFlowUtil.filter(tokens[1]) + ""); } - msg.append("" + (tokens[2]) + ""); - msg.append("" + category + ""); + msg.append("" + PageFlowUtil.filter(tokens[2]) + ""); + msg.append("" + PageFlowUtil.filter(category) + ""); for (FieldDescriptor fd : toShow) { if (totals.containsKey(fd.getFieldName())) { String url = projUrl + fd.getFilter(); - msg.append("" + totals.get(fd.getFieldName()) + ""); + msg.append("" + totals.get(fd.getFieldName()) + ""); } else {