Skip to content
Open
Show file tree
Hide file tree
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 @@ -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
Expand Down Expand Up @@ -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);
}
Comment on lines +66 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing null check for htmlText parameter.

The new overload doesn't handle null input, unlike all other methods in this class (e.g., cleanHtml, cleanI18nString, the other cleanHtmlRelaxed). Passing null will cause a NullPointerException from Jsoup.clean().

🛡️ Proposed fix to add null handling
 public static String cleanHtmlRelaxed(String htmlText, Safelist whitelist) {
+    if (htmlText == null) {
+        return "";
+    }
     Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
     return Jsoup.clean(htmlText, "", whitelist, outputSettings);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 htmlText, Safelist whitelist) {
if (htmlText == null) {
return "";
}
Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
return Jsoup.clean(htmlText, "", whitelist, outputSettings);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sormas-api/src/main/java/de/symeda/sormas/api/utils/HtmlHelper.java` around
lines 66 - 69, The new overload cleanHtmlRelaxed(String htmlText, Safelist
whitelist) lacks a null check for htmlText and will NPE in Jsoup.clean(); update
the method to mirror the other helpers (e.g., cleanHtml, cleanI18nString) by
returning null immediately if htmlText is null before creating
Document.OutputSettings or calling Jsoup.clean, keeping the existing use of
Safelist and 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"));
}

/**
Expand Down
75 changes: 61 additions & 14 deletions sormas-ui/src/main/java/de/symeda/sormas/ui/caze/CaseDataForm.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

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;
Expand Down Expand Up @@ -134,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;
Expand Down Expand Up @@ -220,8 +222,9 @@ public class CaseDataForm extends AbstractEditForm<CaseDataDto> {
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) +
Expand Down Expand Up @@ -1647,27 +1650,71 @@ 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);
// 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;

while (matcher.find()) {
result.append(escapeHtml(text.substring(last, matcher.start())));

String escapedUrl = escapeHtml(matcher.group(1));
result.append("<a href=\"")
.append(escapedUrl)
.append("\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: `#197de1`; text-decoration: underline;\">")
.append(escapedUrl)
.append("</a>");

result.append(sanitizedText, last, matcher.start());

String htmlTag = matcher.group(1);
String url = matcher.group(2);
if (htmlTag != null) {
// This is a rich text tag verified clean by Jsoup. Pass it through safely.
result.append(htmlTag);
} else if (url != null) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// It's a plain-text URL. Wrap it in your custom blue link styling.
String escapedUrl = escapeHtml(url);
result.append("<a href=\"")
.append(escapedUrl)
.append("\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #197de1; text-decoration: underline;\">")
.append(escapedUrl)
.append("</a>");
}
last = matcher.end();
}

result.append(escapeHtml(text.substring(last)));
result.append(sanitizedText.substring(last));
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., &amp;lt; becomes &lt;)
String step1 = value.replace("&amp;", "&");
// Now, safely convert standard HTML entities to real brackets
return step1.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'");
}

/**
* Converting special characters in a string into their safe HTML entity values
*
* @param value
* @return
*/
private static String escapeHtml(String value) {
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
}
Expand Down
Loading