From 7cc9ac22e5e956304db38279836e082431a70bef Mon Sep 17 00:00:00 2001 From: Karnaiah Pesula Date: Tue, 19 May 2026 15:34:09 +0200 Subject: [PATCH 1/3] Displaying the case definition text with the actual format. --- .../symeda/sormas/ui/caze/CaseDataForm.java | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java index 800eb4215e9..85938d5b57b 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java @@ -220,8 +220,9 @@ public class CaseDataForm extends AbstractEditForm { public static final String DIAGNOSIS_CRITERIA_HEADING_LOC = "diagnosisCriteriaHeadingLoc"; public static final String DIAGNOSIS_CRITERIA_SUBHEADING_LOC = "diagnosisCriteriaSubheadingLoc"; public static final String DIAGNOSIS_CRITERIA_LAB_TEST_PANEL_LOC = "diagnosisCriteriaLoc"; - private static final Pattern URL_PATTERN = Pattern.compile("((https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])"); - + private static final Pattern RICH_TEXT_OR_URL_PATTERN = Pattern.compile( + "(<\\/?[a-zA-Z0-9]+(?:\\s+[a-zA-Z0-9\\-]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^'\\\">\\s]+))?)*\\s*\\/?>)|(https?://[^<\\s]+)", + Pattern.CASE_INSENSITIVE); //@formatter:off private static final String MAIN_HTML_LAYOUT = loc(CASE_DATA_HEADING_LOC) + @@ -1647,27 +1648,59 @@ private void getManualCaseDefinition() { * @return sanitized url */ private String sanitizeAndLinkify(String text) { - Matcher matcher = URL_PATTERN.matcher(text); + if (text == null || text.isEmpty()) { + return ""; + } + String htmlText = unescapeHtml(text); + Matcher matcher = RICH_TEXT_OR_URL_PATTERN.matcher(htmlText); StringBuilder result = new StringBuilder(); int last = 0; while (matcher.find()) { - result.append(escapeHtml(text.substring(last, matcher.start()))); - - String escapedUrl = escapeHtml(matcher.group(1)); - result.append("") - .append(escapedUrl) - .append(""); - + String plainTextSegment = htmlText.substring(last, matcher.start()); + result.append(escapeHtml(plainTextSegment).replace("&nbsp;", " ")); + + String htmlTag = matcher.group(1); + String url = matcher.group(2); + if (htmlTag != null) { + // It's a rich text tag (like
or
). Pass it through safely. + result.append(htmlTag); + } else if (url != null) { + // It's a plain-text URL. Wrap it in your custom blue link styling. + String escapedUrl = escapeHtml(url); + result.append("") + .append(escapedUrl) + .append(""); + } last = matcher.end(); } - - result.append(escapeHtml(text.substring(last))); + result.append(escapeHtml(htmlText.substring(last)).replace("&nbsp;", " ")); return result.toString(); } + /** + * Replacing any escape sequence with the character that it represents. + * + * @param value + * @return String + */ + private String unescapeHtml(String value) { + if (value == null) + return ""; + // First, convert any double-escaped amps (e.g., &lt; becomes <) + String step1 = value.replace("&", "&"); + // Now, safely convert standard HTML entities to real brackets + return step1.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'"); + } + + /** + * Converting special characters in a string into their safe HTML entity values + * + * @param value + * @return + */ private static String escapeHtml(String value) { return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'"); } From 088860fa6abb3ce0850ea6b08f5f6607f829b188 Mon Sep 17 00:00:00 2001 From: Karnaiah Pesula Date: Wed, 20 May 2026 10:29:28 +0200 Subject: [PATCH 2/3] Fixed the vulnerability issue --- .../symeda/sormas/ui/caze/CaseDataForm.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java index 85938d5b57b..3efd301362a 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -1656,6 +1657,10 @@ private String sanitizeAndLinkify(String text) { StringBuilder result = new StringBuilder(); int last = 0; + // Expanded the allowed tags to include standard rich text formatting options + Set allowedTags = Set + .of("div", "span", "p", "br", "b", "i", "u", "strong", "em", "ul", "ol", "li", "table", "tr", "td", "th", "thead", "tbody", "font", "a"); + while (matcher.find()) { String plainTextSegment = htmlText.substring(last, matcher.start()); result.append(escapeHtml(plainTextSegment).replace("&nbsp;", " ")); @@ -1663,8 +1668,21 @@ private String sanitizeAndLinkify(String text) { String htmlTag = matcher.group(1); String url = matcher.group(2); if (htmlTag != null) { - // It's a rich text tag (like
or
). Pass it through safely. - result.append(htmlTag); + // Only allow safe formatting tags + String cleanTagName = htmlTag.replaceAll("[<>/]", "").trim().split("\\s+")[0].toLowerCase(); + if (allowedTags.contains(cleanTagName)) { + String lowerTag = htmlTag.toLowerCase(); + if (lowerTag.contains("javascript:") + || lowerTag.contains("onclick") + || lowerTag.contains("onerror") + || lowerTag.contains("onload")) { + // Attack vector found! Escape it safely into text instead of executing it + result.append(escapeHtml(htmlTag)); + } else { + // It's a completely safe rich text element. Pass it through so styles render perfectly. + result.append(htmlTag); + } + } } else if (url != null) { // It's a plain-text URL. Wrap it in your custom blue link styling. String escapedUrl = escapeHtml(url); From d24227b0fbb194dc2db774dd19c71e8ba344617a Mon Sep 17 00:00:00 2001 From: Karnaiah Pesula Date: Wed, 20 May 2026 11:46:40 +0200 Subject: [PATCH 3/3] Fixed the vulnerability issue --- .../symeda/sormas/api/utils/HtmlHelper.java | 25 ++++++++++- .../symeda/sormas/ui/caze/CaseDataForm.java | 44 +++++++++---------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java b/sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java index 1a2397d1335..b320bbaaddf 100644 --- a/sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java +++ b/sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java @@ -18,6 +18,7 @@ package de.symeda.sormas.api.utils; import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; import org.jsoup.safety.Safelist; // This class provides general XSS-Prevention methods using Jsoup.clean @@ -55,8 +56,30 @@ public static String cleanI18nString(String string) { return (string == null) ? "" : Jsoup.clean(string, Safelist.basic()); } + /** + * to whitelist html tags in {@code htmlText} to prevent HTML injection. + * + * @param htmlText + * @param whitelist + * @return + */ + public static String cleanHtmlRelaxed(String htmlText, Safelist whitelist) { + Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false); + return Jsoup.clean(htmlText, "", whitelist, outputSettings); + } + public static String cleanHtmlRelaxed(String string) { - return (string == null) ? "" : Jsoup.clean(string, Safelist.relaxed()); + return (string == null) + ? "" + : Jsoup.clean( + string, + Safelist.relaxed() + .addTags("u", "font") + .addAttributes("font", "size", "color") + .addAttributes("span", "style") + .addAttributes("p", "style") + .addAttributes("div", "style") + .addAttributes("font", "style")); } /** diff --git a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java index 3efd301362a..e5aa5116432 100644 --- a/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java +++ b/sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java @@ -40,13 +40,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.jsoup.safety.Safelist; import com.vaadin.icons.VaadinIcons; import com.vaadin.server.ErrorMessage; @@ -135,6 +135,7 @@ import de.symeda.sormas.api.utils.DataHelper; import de.symeda.sormas.api.utils.DateHelper; import de.symeda.sormas.api.utils.ExtendedReduced; +import de.symeda.sormas.api.utils.HtmlHelper; import de.symeda.sormas.api.utils.YesNoUnknown; import de.symeda.sormas.api.utils.fieldvisibility.FieldVisibilityCheckers; import de.symeda.sormas.api.utils.fieldvisibility.checkers.CountryFieldVisibilityChecker; @@ -1653,36 +1654,31 @@ private String sanitizeAndLinkify(String text) { return ""; } String htmlText = unescapeHtml(text); - Matcher matcher = RICH_TEXT_OR_URL_PATTERN.matcher(htmlText); +// Leveraging existing codebase tool to strip ALL unapproved tags, + Safelist customizedSafelist = Safelist.relaxed() + .addTags("u", "font") + .addAttributes("font", "size", "color") + .addAttributes("span", "style") + .addAttributes("p", "style") + .addAttributes("div", "style") + .addAttributes("font", "style") + .addAttributes("a", "href", "target", "rel", "style") + .addEnforcedAttribute("a", "target", "_blank") + .addEnforcedAttribute("a", "rel", "noopener noreferrer"); + + String sanitizedText = HtmlHelper.cleanHtmlRelaxed(htmlText, customizedSafelist); + Matcher matcher = RICH_TEXT_OR_URL_PATTERN.matcher(sanitizedText); StringBuilder result = new StringBuilder(); int last = 0; - // Expanded the allowed tags to include standard rich text formatting options - Set allowedTags = Set - .of("div", "span", "p", "br", "b", "i", "u", "strong", "em", "ul", "ol", "li", "table", "tr", "td", "th", "thead", "tbody", "font", "a"); - while (matcher.find()) { - String plainTextSegment = htmlText.substring(last, matcher.start()); - result.append(escapeHtml(plainTextSegment).replace("&nbsp;", " ")); + result.append(sanitizedText, last, matcher.start()); String htmlTag = matcher.group(1); String url = matcher.group(2); if (htmlTag != null) { - // Only allow safe formatting tags - String cleanTagName = htmlTag.replaceAll("[<>/]", "").trim().split("\\s+")[0].toLowerCase(); - if (allowedTags.contains(cleanTagName)) { - String lowerTag = htmlTag.toLowerCase(); - if (lowerTag.contains("javascript:") - || lowerTag.contains("onclick") - || lowerTag.contains("onerror") - || lowerTag.contains("onload")) { - // Attack vector found! Escape it safely into text instead of executing it - result.append(escapeHtml(htmlTag)); - } else { - // It's a completely safe rich text element. Pass it through so styles render perfectly. - result.append(htmlTag); - } - } + // This is a rich text tag verified clean by Jsoup. Pass it through safely. + result.append(htmlTag); } else if (url != null) { // It's a plain-text URL. Wrap it in your custom blue link styling. String escapedUrl = escapeHtml(url); @@ -1694,7 +1690,7 @@ private String sanitizeAndLinkify(String text) { } last = matcher.end(); } - result.append(escapeHtml(htmlText.substring(last)).replace("&nbsp;", " ")); + result.append(sanitizedText.substring(last)); return result.toString(); }