From 797ffe6c509de6db1e95393f7327b12e0ea958e7 Mon Sep 17 00:00:00 2001 From: Machac Date: Wed, 15 Apr 2026 10:02:41 +0200 Subject: [PATCH 1/5] [NAE-2408] Implement field sanitization modes for text data fields - Introduces `IFieldSanitizationService` and its implementation. - Adds configuration for HTML sanitization modes and actions. - Updates `DataService` to sanitize text fields during data modification. - Includes unit and integration tests for sanitization behavior. - Bumps project version to 6.4.3-SNAPSHOT. --- pom.xml | 7 +- .../interfaces/IFieldSanitizationService.java | 10 + .../FieldSanitizationService.java | 140 ++++ .../sanitization/SanitizationAction.java | 20 + .../sanitization/SanitizationMode.java | 27 + .../FieldSanitizationIntegrationTest.java | 634 +++++++++++++++++ .../service/FieldSanitizationServiceTest.java | 332 +++++++++ src/test/resources/data_text_validation.xml | 2 +- src/test/resources/sanitization_modes.xml | 655 ++++++++++++++++++ 9 files changed, 1825 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldSanitizationService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationAction.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java create mode 100644 src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java create mode 100644 src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java create mode 100644 src/test/resources/sanitization_modes.xml diff --git a/pom.xml b/pom.xml index d9f4b8ffac1..dd85fb1bbc7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.netgrif application-engine - 6.4.2 + 6.4.3-SNAPSHOT jar NETGRIF Application Engine @@ -233,6 +233,11 @@ 0.9.1 + + com.googlecode.owasp-java-html-sanitizer + owasp-java-html-sanitizer + 20260313.1 + diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldSanitizationService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldSanitizationService.java new file mode 100644 index 00000000000..9a69359f478 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IFieldSanitizationService.java @@ -0,0 +1,10 @@ +package com.netgrif.application.engine.workflow.service.interfaces; + + +import com.netgrif.application.engine.petrinet.domain.dataset.Field; + +public interface IFieldSanitizationService { + + String sanitize(String value, Field field); + +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java new file mode 100644 index 00000000000..10f39aab0f6 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java @@ -0,0 +1,140 @@ +package com.netgrif.application.engine.workflow.service.sanitization; + +import com.netgrif.application.engine.petrinet.domain.Component; +import com.netgrif.application.engine.petrinet.domain.dataset.Field; +import com.netgrif.application.engine.workflow.service.interfaces.IFieldSanitizationService; +import lombok.extern.slf4j.Slf4j; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; +import org.owasp.html.Sanitizers; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Slf4j +@Service +public class FieldSanitizationService implements IFieldSanitizationService { + + public static final String SANITIZATION_MODE_KEY = "sanitizationMode"; + public static final String SANITIZATION_ACTION_KEY = "sanitizationAction"; + + private static final PolicyFactory PLAIN_TEXT_POLICY = + new HtmlPolicyBuilder().toFactory(); + + private static final PolicyFactory SAFE_HTML_BASIC_POLICY = + new HtmlPolicyBuilder() + .allowElements("b", "i", "u", "em", "strong", "s", "span", "br") + .toFactory(); + + private static final PolicyFactory SAFE_HTML_LINKS_ONLY_POLICY = + new HtmlPolicyBuilder() + .allowElements("a") + .allowAttributes("href").onElements("a") + .allowUrlProtocols("http", "https", "mailto") + .requireRelNofollowOnLinks() + .toFactory(); + + private static final PolicyFactory SAFE_HTML_NO_LINKS_POLICY = + Sanitizers.FORMATTING.and(Sanitizers.BLOCKS); + + private static final PolicyFactory SAFE_HTML_POLICY = + Sanitizers.FORMATTING + .and(Sanitizers.LINKS) + .and(Sanitizers.BLOCKS); + + private static final PolicyFactory SAFE_HTML_RELAXED_POLICY = + Sanitizers.FORMATTING + .and(Sanitizers.LINKS) + .and(Sanitizers.BLOCKS) + .and(Sanitizers.TABLES) + .and(new HtmlPolicyBuilder() + .allowElements("code", "pre", "span") + .toFactory()); + + private static final PolicyFactory DISABLE_JAVASCRIPT_POLICY = + new HtmlPolicyBuilder() + .allowElements( + "a", "abbr", "b", "blockquote", "br", "caption", "code", "col", "colgroup", + "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", + "hr", "i", "img", "ins", "li", "ol", "p", "pre", "s", "small", "span", + "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", + "tr", "u", "ul" + ) + .allowWithoutAttributes( + "abbr", "b", "blockquote", "br", "caption", "code", "dd", "del", "div", "dl", + "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", + "li", "ol", "p", "pre", "s", "small", "span", "strong", "sub", "sup", + "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul" + ) + .allowAttributes("href").onElements("a") + .allowAttributes("src", "alt", "title").onElements("img") + .allowAttributes("colspan", "rowspan").onElements("td", "th") + .allowUrlProtocols("http", "https", "mailto") + .requireRelNofollowOnLinks() + .toFactory(); + + @Override + public String sanitize(String value, Field field) { + if (value == null) { + return null; + } + + Component component = field.getComponent(); + SanitizationMode mode = SanitizationMode.from(getProperty(component, SANITIZATION_MODE_KEY)); + SanitizationAction action = SanitizationAction.from(getProperty(component, SANITIZATION_ACTION_KEY)); + + if (mode == SanitizationMode.OFF) { + log.debug("Sanitization mode OFF for field [{}]", field.getStringId()); + return value; + } + + String sanitized = resolvePolicy(mode).sanitize(value); + + if (!value.equals(sanitized)) { + log.warn("Content was modified by sanitizer for field [{}] (mode={}, action={})", + field.getStringId(), mode, action); + if (action == SanitizationAction.REJECT) { + throw new IllegalArgumentException( + "Field [" + field.getStringId() + "] contains unsafe content " + + "and the configured action is REJECT." + ); + } + } + + return sanitized; + } + + protected PolicyFactory resolvePolicy(SanitizationMode mode) { + switch (mode) { + case SAFE_HTML: + return SAFE_HTML_POLICY; + case SAFE_HTML_BASIC: + return SAFE_HTML_BASIC_POLICY; + case SAFE_HTML_LINKS_ONLY: + return SAFE_HTML_LINKS_ONLY_POLICY; + case SAFE_HTML_NO_LINKS: + return SAFE_HTML_NO_LINKS_POLICY; + case SAFE_HTML_RELAXED: + return SAFE_HTML_RELAXED_POLICY; + case DISABLE_JAVASCRIPT: + return DISABLE_JAVASCRIPT_POLICY; + case PLAIN_TEXT: + default: + return PLAIN_TEXT_POLICY; + } + } + + protected String getProperty(Component component, String key) { + if (component == null) { + return null; + } + + Map properties = component.getProperties(); + if (properties == null) { + return null; + } + + return properties.get(key); + } + +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationAction.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationAction.java new file mode 100644 index 00000000000..15673e48ba9 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationAction.java @@ -0,0 +1,20 @@ +package com.netgrif.application.engine.workflow.service.sanitization; + +public enum SanitizationAction { + SANITIZE, + REJECT; + + static SanitizationAction from(String value) { + if (value == null || value.isBlank()) { + return SANITIZE; + } + + for (SanitizationAction action : values()) { + if (action.name().equalsIgnoreCase(value)) { + return action; + } + } + + return SANITIZE; + } +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java new file mode 100644 index 00000000000..d389a1ef14d --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java @@ -0,0 +1,27 @@ +package com.netgrif.application.engine.workflow.service.sanitization; + +public enum SanitizationMode { + + OFF, + PLAIN_TEXT, + SAFE_HTML, + SAFE_HTML_BASIC, + SAFE_HTML_LINKS_ONLY, + SAFE_HTML_NO_LINKS, + DISABLE_JAVASCRIPT, + SAFE_HTML_RELAXED; + + public static SanitizationMode from(String value) { + if (value == null || value.isBlank()) { + return PLAIN_TEXT; + } + + for (SanitizationMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + + return PLAIN_TEXT; + } +} \ No newline at end of file diff --git a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java new file mode 100644 index 00000000000..19746657374 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java @@ -0,0 +1,634 @@ +package com.netgrif.application.engine.workflow.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.VersionType; +import com.netgrif.application.engine.petrinet.domain.throwable.MissingPetriNetMetaDataException; +import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; +import com.netgrif.application.engine.startup.SuperCreator; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.DataField; +import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.domain.eventoutcomes.caseoutcomes.CreateCaseEventOutcome; +import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; +import com.netgrif.application.engine.workflow.domain.eventoutcomes.petrinetoutcomes.ImportPetriNetEventOutcome; +import com.netgrif.application.engine.workflow.domain.repositories.CaseRepository; +import com.netgrif.application.engine.workflow.domain.repositories.TaskRepository; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.FileInputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class FieldSanitizationIntegrationTest { + + private static final String XML_PATH = "src/test/resources/sanitization_modes.xml"; + private static final String TASK_TITLE = "Transition"; + + private static final String PAYLOAD_SCRIPT_BOLD = "Hello"; + private static final String PAYLOAD_IMG_ONERROR = "

"; + private static final String PAYLOAD_JS_LINK = "click"; + private static final String PAYLOAD_SAFE_LINK = "click"; + private static final String PAYLOAD_TABLE_SCRIPT = "
Cell
"; + private static final String PAYLOAD_CODE_PRE = "System.out.println()
line1\nline2
"; + private static final String PAYLOAD_PLAIN = "Hello plain text"; + + @Autowired + private TestHelper testHelper; + + @Autowired + private IPetriNetService petriNetService; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private CaseRepository caseRepository; + + @Autowired + private DataService dataService; + + @Autowired + private SuperCreator superCreator; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void before() { + testHelper.truncateDbs(); + } + + @Test + void offModeShouldStoreRawScriptPayload() throws Exception { + assertStoredEquals("text_off", PAYLOAD_SCRIPT_BOLD, PAYLOAD_SCRIPT_BOLD); + } + + @Test + void offModeShouldStoreRawImgOnErrorPayload() throws Exception { + assertStoredEquals("text_off", PAYLOAD_IMG_ONERROR, PAYLOAD_IMG_ONERROR); + } + + @Test + void offModeShouldStoreRawJavascriptHrefPayload() throws Exception { + assertStoredEquals("text_off", PAYLOAD_JS_LINK, PAYLOAD_JS_LINK); + } + + @Test + void offModeShouldKeepPlainTextUntouched() throws Exception { + assertStoredEquals("text_off", PAYLOAD_PLAIN, PAYLOAD_PLAIN); + } + + @Test + void plainDefaultShouldStripHtmlFromScriptPayload() throws Exception { + assertStoredEquals("text_plain_default", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void plainDefaultShouldRemoveImgPayload() throws Exception { + assertStoredEquals("text_plain_default", PAYLOAD_IMG_ONERROR, ""); + } + + @Test + void plainDefaultShouldKeepPlainTextUntouched() throws Exception { + assertStoredEquals("text_plain_default", PAYLOAD_PLAIN, PAYLOAD_PLAIN); + } + + @Test + void plainTextSanitizeShouldStripHtml() throws Exception { + assertStoredEquals("text_plain_sanitize", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void plainTextSanitizeShouldKeepPlainTextUntouched() throws Exception { + assertStoredEquals("text_plain_sanitize", "Hello world 123", "Hello world 123"); + } + + @Test + void plainTextSanitizeShouldRemoveImgTagAndKeepNoText() throws Exception { + assertStoredEquals("text_plain_sanitize", PAYLOAD_IMG_ONERROR, ""); + } + + @Test + void plainTextSanitizeShouldRemoveImgTagAndKeepText() throws Exception { + assertStoredEquals("text_plain_sanitize", "test", "test"); + } + + @Test + void plainTextRejectShouldThrowExceptionForBoldHtml() throws Exception { + assertRejected("text_plain_reject", "Hello"); + } + + @Test + void plainTextRejectShouldThrowExceptionForScriptPayload() throws Exception { + assertRejected("text_plain_reject", PAYLOAD_SCRIPT_BOLD); + } + + @Test + void plainTextRejectShouldThrowExceptionForImgPayload() throws Exception { + assertRejected("text_plain_reject", PAYLOAD_IMG_ONERROR); + } + + @Test + void plainTextRejectShouldNotThrowWhenValueIsAlreadyClean() throws Exception { + assertStoredEquals("text_plain_reject", "Hello clean", "Hello clean"); + } + + @Test + void safeHtmlShouldKeepSafeFormattingAndRemoveScript() throws Exception { + assertStoredEquals("text_safe_html", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void safeHtmlShouldRemoveImgOnError() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_html", PAYLOAD_IMG_ONERROR)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_html"); + assertFalse(value.contains("Hello

")); + assertTrue(value.contains("
    ")); + assertTrue(value.contains("
  • One
  • ")); + assertTrue(value.contains("
  • Two
  • ")); + } + + @Test + void safeHtmlShouldRemoveJavascriptHref() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_html", PAYLOAD_JS_LINK)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_html"); + assertFalse(value.contains("javascript:")); + assertTrue(value.contains("click")); + } + + @Test + void safeHtmlShouldKeepSafeHttpsLink() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_html", PAYLOAD_SAFE_LINK)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_html"); + assertTrue(value.contains("click")); + assertTrue(value.contains("https://example.com")); + } + + @Test + void safeHtmlRejectShouldThrowExceptionWhenUnsafeContentIsPresent() throws Exception { + assertRejected("text_safe_html_reject", PAYLOAD_SCRIPT_BOLD); + } + + @Test + void safeHtmlRejectShouldThrowExceptionForImgPayload() throws Exception { + assertRejected("text_safe_html_reject", PAYLOAD_IMG_ONERROR); + } + + @Test + void safeHtmlRejectShouldNotThrowWhenContentIsSafe() throws Exception { + assertStoredEquals("text_safe_html_reject", "Helloworld", "Helloworld"); + } + + @Test + void safeHtmlBasicShouldKeepInlineFormattingOnly() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + String input = "Helloworld
    BLOCK
    "; + dataService.setData(task, textValue("text_safe_basic", input)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_basic"); + assertTrue(value.contains("Hello")); + assertTrue(value.contains("world")); + assertFalse(value.contains("
    ")); + assertTrue(value.contains("BLOCK")); + } + + @Test + void safeHtmlBasicShouldRemoveImgOnError() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_basic", PAYLOAD_IMG_ONERROR)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_basic"); + assertFalse(value.contains("click"; + dataService.setData(task, textValue("text_safe_links_only", input)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_links_only"); + assertFalse(value.contains("")); + assertTrue(value.contains("Hello")); + assertTrue(value.contains("click")); + } + + @Test + void safeHtmlLinksOnlyShouldRemoveImgPayload() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_links_only", PAYLOAD_IMG_ONERROR)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_links_only"); + assertFalse(value.contains("clickHello"; + dataService.setData(task, textValue("text_safe_no_links", input)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_no_links"); + assertFalse(value.contains("Hello")); + } + + @Test + void safeHtmlNoLinksShouldKeepBlockElements() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + String input = "

    Hello

    World
    "; + dataService.setData(task, textValue("text_safe_no_links", input)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_no_links"); + assertTrue(value.contains("

    Hello

    ")); + assertTrue(value.contains("
    World
    ")); + } + + @Test + void safeHtmlNoLinksShouldRemoveJavascriptLinkButKeepText() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_safe_no_links", PAYLOAD_JS_LINK)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_no_links"); + assertFalse(value.contains("Cell")); + + String value = getStringValue(getCase(useCase.getStringId()), "text_safe_relaxed"); + assertTrue(value.contains("System.out.println()")); + assertTrue(value.contains("
    "));
    +    }
    +
    +    @Test
    +    void safeHtmlRelaxedShouldStillRemoveScript() throws Exception {
    +        Case useCase = createCase();
    +        Task task = findTask(useCase);
    +
    +        dataService.setData(task, textValue("text_safe_relaxed", PAYLOAD_TABLE_SCRIPT));
    +
    +        String value = getStringValue(getCase(useCase.getStringId()), "text_safe_relaxed");
    +        assertTrue(value.contains(""));
    +    }
    +
    +    @Test
    +    void safeHtmlRelaxedShouldRemoveImgOnError() throws Exception {
    +        Case useCase = createCase();
    +        Task task = findTask(useCase);
    +
    +        dataService.setData(task, textValue("text_safe_relaxed", PAYLOAD_IMG_ONERROR));
    +
    +        String value = getStringValue(getCase(useCase.getStringId()), "text_safe_relaxed");
    +        assertFalse(value.contains("Cell");
    +    }
    +
    +    @Test
    +    void disableJavascriptShouldRemoveEventHandlerButKeepMarkup() throws Exception {
    +        Case useCase = createCase();
    +        Task task = findTask(useCase);
    +
    +        dataService.setData(task, textValue("text_disable_javascript", PAYLOAD_IMG_ONERROR));
    +
    +        String value = getStringValue(getCase(useCase.getStringId()), "text_disable_javascript");
    +        assertTrue(value.contains("

    ")); + assertTrue(value.contains("

    "); + } + + @Test + void dataLevelSafeHtmlShouldApplyConfiguration() throws Exception { + assertStoredEquals("text_safe_html", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void dataLevelSafeHtmlShouldKeepSafeHtmlUntouched() throws Exception { + assertStoredEquals("text_safe_html", "Helloworld", "Helloworld"); + } + + @Test + void dataRefOverrideOffShouldOverrideDataConfiguration() throws Exception { + assertStoredEquals("text_override_off", PAYLOAD_SCRIPT_BOLD, PAYLOAD_SCRIPT_BOLD); + } + + @Test + void dataRefOverridePlainSanitizeShouldOverrideDataConfiguration() throws Exception { + assertStoredEquals("text_override_plain_sanitize", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void dataRefOverridePlainRejectShouldRejectUnsafeInput() throws Exception { + assertRejected("text_override_plain_reject", PAYLOAD_SCRIPT_BOLD); + } + + @Test + void dataRefOverrideSafeHtmlShouldOverridePlainDefaultConfiguration() throws Exception { + assertStoredEquals("text_override_safe_html", PAYLOAD_SCRIPT_BOLD, "Hello"); + } + + @Test + void dataRefOverrideSafeHtmlRejectShouldRejectUnsafeInput() throws Exception { + assertRejected("text_override_safe_html_reject", PAYLOAD_SCRIPT_BOLD); + } + + @Test + void dataRefOverrideSafeBasicShouldOverrideDefaultConfiguration() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_override_safe_basic", "Helloworld
    BLOCK
    ")); + + String value = getStringValue(getCase(useCase.getStringId()), "text_override_safe_basic"); + assertTrue(value.contains("Hello")); + assertTrue(value.contains("world")); + assertFalse(value.contains("
    ")); + assertTrue(value.contains("BLOCK")); + } + + @Test + void dataRefOverrideSafeLinksOnlyShouldKeepOnlyLinks() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_override_safe_links_only", "Helloclick")); + + String value = getStringValue(getCase(useCase.getStringId()), "text_override_safe_links_only"); + assertFalse(value.contains("")); + assertTrue(value.contains("Hello")); + assertTrue(value.contains("click")); + assertTrue(value.contains("https://example.com")); + } + + @Test + void dataRefOverrideSafeNoLinksShouldRemoveAnchorButKeepText() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_override_safe_no_links", "clickHello")); + + String value = getStringValue(getCase(useCase.getStringId()), "text_override_safe_no_links"); + assertFalse(value.contains("Hello")); + } + + @Test + void dataRefOverrideSafeRelaxedShouldAcceptTable() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_override_safe_relaxed", "
    Cell
    ")); + + String value = getStringValue(getCase(useCase.getStringId()), "text_override_safe_relaxed"); + assertTrue(value.contains("Cell"); + } + + @Test + void dataRefOverrideDisableJavascriptShouldRemoveEventHandlerButKeepMarkup() throws Exception { + Case useCase = createCase(); + Task task = findTask(useCase); + + dataService.setData(task, textValue("text_override_disable_javascript", PAYLOAD_IMG_ONERROR)); + + String value = getStringValue(getCase(useCase.getStringId()), "text_override_disable_javascript"); + assertTrue(value.contains("

    ")); + assertTrue(value.contains(" dataService.setData(task, textValue(fieldId, input)) + ); + + assertTrue(ex.getMessage().contains("unsafe content")); + } + + private Case createCase() throws IOException, MissingPetriNetMetaDataException { + ImportPetriNetEventOutcome importOutcome = petriNetService.importPetriNet( + new FileInputStream(XML_PATH), + VersionType.MAJOR, + superCreator.getLoggedSuper() + ); + + PetriNet net = importOutcome.getNet(); + assertNotNull(net); + + CreateCaseEventOutcome caseOutcome = workflowService.createCase( + net.getStringId(), + "Sanitization test case", + "color", + superCreator.getLoggedSuper() + ); + + assertNotNull(caseOutcome); + assertNotNull(caseOutcome.getCase()); + return caseOutcome.getCase(); + } + + private Task findTask(Case useCase) { + return taskRepository.findAll() + .stream() + .filter(task -> useCase.getStringId().equals(task.getCaseId())) + .filter(task -> task.getTitle() != null && TASK_TITLE.equals(task.getTitle().getDefaultValue())) + .findFirst() + .orElseThrow(() -> new AssertionError("Task not found")); + } + + private Case getCase(String caseId) { + return caseRepository.findById(caseId) + .orElseThrow(() -> new AssertionError("Case not found")); + } + + private String getStringValue(Case useCase, String fieldId) { + DataField dataField = useCase.getDataField(fieldId); + assertNotNull(dataField, "Field [" + fieldId + "] not found"); + return (String) dataField.getValue(); + } + + private ObjectNode textValue(String fieldId, String value) { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode fieldNode = objectMapper.createObjectNode(); + fieldNode.put("type", "text"); + fieldNode.put("value", value); + root.set(fieldId, fieldNode); + return root; + } + + private ObjectNode textValueWithNull(String fieldId) { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode fieldNode = objectMapper.createObjectNode(); + fieldNode.put("type", "text"); + fieldNode.putNull("value"); + root.set(fieldId, fieldNode); + return root; + } +} \ No newline at end of file diff --git a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java new file mode 100644 index 00000000000..fda41288c40 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java @@ -0,0 +1,332 @@ +package com.netgrif.application.engine.workflow.service; + +import com.netgrif.application.engine.workflow.service.sanitization.FieldSanitizationService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import com.netgrif.application.engine.petrinet.domain.Component; +import com.netgrif.application.engine.petrinet.domain.dataset.TextField; +import org.mockito.Mockito; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@Slf4j +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class FieldSanitizationServiceTest { + + private FieldSanitizationService service; + + @BeforeEach + void setUp() { + service = new FieldSanitizationService(); + } + + @Test + void shouldReturnNullWhenValueIsNull() { + TextField field = new TextField(); + String result = service.sanitize(null, field); + assertNull(result); + } + + @Test + void shouldReturnSameValueWhenInputIsPlainText() { + TextField field = new TextField(); + String input = "Hello world 123"; + String result = service.sanitize(input, field); + assertEquals("Hello world 123", result); + } + + @Test + void shouldStripHtmlTagsWhenDefaultModeIsPlainText() { + TextField field = new TextField(); + String input = "Hello world"; + String result = service.sanitize(input, field); + assertEquals("Hello world", result); + assertNotEquals(input, result); + } + + @Test + void shouldStripDangerousAttributesWhenDefaultModeIsPlainText() { + TextField field = new TextField(); + String input = "test"; + String result = service.sanitize(input, field); + assertEquals("test", result); + } + + @Test + void shouldNotSanitizeWhenModeIsOff() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "OFF") + ); + field.setComponent(component); + String input = "Hello"; + String result = service.sanitize(input, field); + assertEquals(input, result); + } + + @Test + void shouldResolveModeCaseInsensitively() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "oFf") + ); + + field.setComponent(component); + + String input = "Hello"; + String result = service.sanitize(input, field); + + assertEquals(input, result); + } + + @Test + void shouldUsePlainTextModeWhenComponentIsNull() { + TextField field = new TextField(); + field.setComponent(null); + String input = "Hello"; + String result = service.sanitize(input, field); + assertEquals("Hello", result); + } + + @Test + void shouldUsePlainTextModeWhenComponentPropertiesAreNull() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn(null); + field.setComponent(component); + String input = "Hello"; + String result = service.sanitize(input, field); + assertEquals("Hello", result); + } + + @Test + void shouldUsePlainTextModeWhenSanitizationModePropertyIsMissing() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn(Map.of("otherKey", "true")); + field.setComponent(component); + String input = "text"; + String result = service.sanitize(input, field); + assertEquals("text", result); + } + + @Test + void shouldKeepSafeFormattingWhenModeIsSafeHtml() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "SAFE_HTML") + ); + + field.setComponent(component); + + String input = "Hello world"; + String result = service.sanitize(input, field); + + assertEquals("Hello world", result); + } + + @Test + void shouldRemoveScriptWhenModeIsSafeHtml() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "SAFE_HTML") + ); + + field.setComponent(component); + + String input = "Hello"; + String result = service.sanitize(input, field); + + assertEquals("Hello", result); + } + + @Test + void shouldRemoveJavascriptHrefWhenModeIsSafeHtml() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "SAFE_HTML") + ); + + field.setComponent(component); + + String input = "click"; + String result = service.sanitize(input, field); + + assertNotEquals(input, result); + assertFalse(result.contains("javascript:")); + assertTrue(result.contains("click")); + } + + + + @Test + void shouldRemoveImgTagWhenModeIsSafeHtml() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "SAFE_HTML") + ); + + field.setComponent(component); + + String input = "test"; + String result = service.sanitize(input, field); + + assertEquals("test", result); + } + + @Test + void shouldThrowExceptionWhenActionIsRejectAndPlainTextModeChangesContent() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "PLAIN_TEXT", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "REJECT" + ) + ); + field.setComponent(component); + String input = "Hello"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> service.sanitize(input, field) + ); + + assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenActionIsRejectAndSafeHtmlModeChangesContent() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "SAFE_HTML", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "REJECT" + ) + ); + + field.setComponent(component); + + String input = "Hello"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> service.sanitize(input, field) + ); + + assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + } + + @Test + void shouldNotThrowExceptionWhenActionIsSanitize() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "PLAIN_TEXT", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "SANITIZE" + ) + ); + field.setComponent(component); + String input = "Hello"; + String result = service.sanitize(input, field); + assertEquals("Hello", result); + } + + @Test + void shouldUseSanitizeActionWhenActionPropertyIsMissing() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + + when(component.getProperties()).thenReturn( + Map.of(FieldSanitizationService.SANITIZATION_MODE_KEY, "PLAIN_TEXT") + ); + + field.setComponent(component); + String input = "Hello"; + String result = service.sanitize(input, field); + assertEquals("Hello", result); + } + + @Test + void shouldResolveActionCaseInsensitively() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "PLAIN_TEXT", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "rEjEcT" + ) + ); + field.setComponent(component); + + String input = "Hello"; + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> service.sanitize(input, field) + ); + + assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + } + + @Test + void shouldNotThrowWhenActionIsRejectButContentIsAlreadyClean() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "PLAIN_TEXT", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "REJECT" + ) + ); + field.setComponent(component); + + String input = "Hello world"; + String result = service.sanitize(input, field); + + assertEquals("Hello world", result); + } + + @Test + void shouldIgnoreRejectActionWhenModeIsOff() { + TextField field = new TextField(); + Component component = Mockito.mock(Component.class); + when(component.getProperties()).thenReturn( + Map.of( + FieldSanitizationService.SANITIZATION_MODE_KEY, "OFF", + FieldSanitizationService.SANITIZATION_ACTION_KEY, "REJECT" + ) + ); + field.setComponent(component); + + String input = "Hello"; + String result = service.sanitize(input, field); + + assertEquals(input, result); + } +} \ No newline at end of file diff --git a/src/test/resources/data_text_validation.xml b/src/test/resources/data_text_validation.xml index cf90b3a361e..0c2c165283f 100644 --- a/src/test/resources/data_text_validation.xml +++ b/src/test/resources/data_text_validation.xml @@ -1,4 +1,4 @@ -F + test diff --git a/src/test/resources/sanitization_modes.xml b/src/test/resources/sanitization_modes.xml new file mode 100644 index 00000000000..1ce133ba871 --- /dev/null +++ b/src/test/resources/sanitization_modes.xml @@ -0,0 +1,655 @@ + + 1 + TES + Data test + true + false + false + + + text_plain_default + Plain default + Plain default + Default plain text sanitization + text + + + + text_off + OFF + + textarea + OFF + SANITIZE + + + + + text_plain_sanitize + PLAIN_TEXT SANITIZE + + textarea + PLAIN_TEXT + SANITIZE + + + + + text_plain_reject + PLAIN_TEXT REJECT + + textarea + PLAIN_TEXT + REJECT + + + + + text_safe_basic + SAFE_HTML_BASIC SANITIZE + + textarea + SAFE_HTML_BASIC + SANITIZE + + + + + text_safe_links_only + SAFE_HTML_LINKS_ONLY SANITIZE + + textarea + SAFE_HTML_LINKS_ONLY + SANITIZE + + + + + text_safe_no_links + SAFE_HTML_NO_LINKS SANITIZE + + htmltextarea + SAFE_HTML_NO_LINKS + SANITIZE + + + + + text_safe_html + SAFE_HTML SANITIZE + + htmltextarea + SAFE_HTML + SANITIZE + + + + + text_safe_html_reject + SAFE_HTML REJECT + + htmltextarea + SAFE_HTML + REJECT + + + + + text_safe_relaxed + SAFE_HTML_RELAXED SANITIZE + + richtextarea + SAFE_HTML_RELAXED + SANITIZE + + + + + text_safe_relaxed_reject + SAFE_HTML_RELAXED REJECT + + richtextarea + SAFE_HTML_RELAXED + REJECT + + + + + text_disable_javascript + DISABLE_JAVASCRIPT SANITIZE + + htmltextarea + DISABLE_JAVASCRIPT + SANITIZE + + + + + text_disable_javascript_reject + DISABLE_JAVASCRIPT REJECT + + htmltextarea + DISABLE_JAVASCRIPT + REJECT + + + + + text_override_off + Override OFF + text + + + + text_override_plain_sanitize + Override PLAIN_TEXT SANITIZE + text + + + + text_override_plain_reject + Override PLAIN_TEXT REJECT + text + + + + text_override_safe_basic + Override SAFE_HTML_BASIC SANITIZE + text + + + + text_override_safe_links_only + Override SAFE_HTML_LINKS_ONLY SANITIZE + text + + + + text_override_safe_no_links + Override SAFE_HTML_NO_LINKS SANITIZE + text + + + + text_override_safe_html + Override SAFE_HTML SANITIZE + text + + + + text_override_safe_html_reject + Override SAFE_HTML REJECT + text + + + + text_override_safe_relaxed + Override SAFE_HTML_RELAXED SANITIZE + text + + + + text_override_safe_relaxed_reject + Override SAFE_HTML_RELAXED REJECT + text + + + + text_override_disable_javascript + Override DISABLE_JAVASCRIPT SANITIZE + text + + + + text_override_disable_javascript_reject + Override DISABLE_JAVASCRIPT REJECT + text + + + + 1 + 48 + 48 + + + NewDataGroup + true + + + text_plain_default + + editable + + + 0 + 0 + 1 + 4 + + outline + + + + + text_off + + editable + + + 0 + 1 + 1 + 4 + + outline + + + + + text_plain_sanitize + + editable + + + 0 + 2 + 1 + 4 + + outline + + + + + text_plain_reject + + editable + + + 0 + 3 + 1 + 4 + + outline + + + + + text_safe_basic + + editable + + + 0 + 4 + 2 + 4 + + outline + + + + + text_safe_links_only + + editable + + + 0 + 6 + 2 + 4 + + outline + + + + + text_safe_no_links + + editable + + + 0 + 8 + 2 + 4 + + outline + + + + + text_safe_html + + editable + + + 0 + 10 + 2 + 4 + + outline + + + + + text_safe_html_reject + + editable + + + 0 + 12 + 2 + 4 + + outline + + + + + text_safe_relaxed + + editable + + + 0 + 14 + 2 + 4 + + outline + + + + + text_safe_relaxed_reject + + editable + + + 0 + 16 + 2 + 4 + + outline + + + + + text_disable_javascript + + editable + + + 0 + 18 + 2 + 4 + + outline + + + + + text_disable_javascript_reject + + editable + + + 0 + 20 + 2 + 4 + + outline + + + + + text_override_off + + editable + + + 0 + 22 + 2 + 4 + + outline + + + textarea + OFF + SANITIZE + + + + + text_override_plain_sanitize + + editable + + + 0 + 24 + 2 + 4 + + outline + + + textarea + PLAIN_TEXT + SANITIZE + + + + + text_override_plain_reject + + editable + + + 0 + 26 + 2 + 4 + + outline + + + textarea + PLAIN_TEXT + REJECT + + + + + text_override_safe_basic + + editable + + + 0 + 28 + 2 + 4 + + outline + + + textarea + SAFE_HTML_BASIC + SANITIZE + + + + + text_override_safe_links_only + + editable + + + 0 + 30 + 2 + 4 + + outline + + + textarea + SAFE_HTML_LINKS_ONLY + SANITIZE + + + + + text_override_safe_no_links + + editable + + + 0 + 32 + 2 + 4 + + outline + + + htmltextarea + SAFE_HTML_NO_LINKS + SANITIZE + + + + + text_override_safe_html + + editable + + + 0 + 34 + 2 + 4 + + outline + + + htmltextarea + SAFE_HTML + SANITIZE + + + + + text_override_safe_html_reject + + editable + + + 0 + 36 + 2 + 4 + + outline + + + htmltextarea + SAFE_HTML + REJECT + + + + + text_override_safe_relaxed + + editable + + + 0 + 38 + 2 + 4 + + outline + + + richtextarea + SAFE_HTML_RELAXED + SANITIZE + + + + + text_override_safe_relaxed_reject + + editable + + + 0 + 40 + 2 + 4 + + outline + + + richtextarea + SAFE_HTML_RELAXED + REJECT + + + + + text_override_disable_javascript + + editable + + + 0 + 42 + 2 + 4 + + outline + + + htmltextarea + DISABLE_JAVASCRIPT + SANITIZE + + + + + text_override_disable_javascript_reject + + editable + + + 0 + 44 + 2 + 4 + + outline + + + htmltextarea + DISABLE_JAVASCRIPT + REJECT + + + + + + \ No newline at end of file From e19f7a161606ff387a5d3bc24d6ad95d5cffab28 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 15 Apr 2026 10:28:58 +0200 Subject: [PATCH 2/5] [NAE-2408] Implement field sanitization modes for text data fields Introduced a sanitization mechanism for text fields using IFieldSanitizationService, ensuring that input values are sanitized before being processed. This includes methods to resolve the appropriate field and component for sanitization. Updated the data parsing logic to integrate the sanitization step. --- .../engine/workflow/service/DataService.java | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index 548ee926a2b..4a3cee41c4f 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -19,6 +19,7 @@ import com.netgrif.application.engine.petrinet.domain.Component; import com.netgrif.application.engine.petrinet.domain.*; import com.netgrif.application.engine.petrinet.domain.dataset.*; +import com.netgrif.application.engine.petrinet.domain.dataset.TextField; import com.netgrif.application.engine.petrinet.domain.dataset.logic.ChangedField; import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior; import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.FieldActionsRunner; @@ -32,10 +33,7 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.GetDataEventOutcome; import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.GetDataGroupsEventOutcome; import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome; -import com.netgrif.application.engine.workflow.service.interfaces.IDataService; -import com.netgrif.application.engine.workflow.service.interfaces.IEventService; -import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; -import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import com.netgrif.application.engine.workflow.service.interfaces.*; import com.netgrif.application.engine.workflow.web.responsebodies.DataFieldsResource; import com.netgrif.application.engine.workflow.web.responsebodies.LocalisedField; import com.querydsl.core.types.Predicate; @@ -100,6 +98,9 @@ public class DataService implements IDataService { @Autowired protected IValidationService validation; + @Autowired + protected IFieldSanitizationService sanitizationService; + @Autowired private StorageResolverService storageResolverService; @@ -279,6 +280,8 @@ public SetDataEventOutcome setData(Task task, ObjectNode values, Map getChangedFieldByFileFieldContainer(String fieldId, Task referencingTask, Case useCase, Map params) { List outcomes = new ArrayList<>(); - outcomes.addAll( resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, + outcomes.addAll(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.PRE, useCase, referencingTask, params)); outcomes.addAll(resolveDataEvents(useCase.getPetriNet().getField(fieldId).get(), DataEventType.SET, EventPhase.POST, useCase, referencingTask, params)); @@ -1157,4 +1160,37 @@ public void validateCaseRefValue(List value, List allowedNets) t // } // } -} + protected Object sanitizeValueIfNeeded(Case useCase, Task task, Field field, Object value) { + if (!(value instanceof String) || !(field instanceof TextField)) { + return value; + } + + return sanitizationService.sanitize((String) value, resolveFieldForSanitization(useCase, task, field)); + } + + protected Field resolveFieldForSanitization(Case useCase, Task task, Field originalField) { + Field field = originalField.clone(); + field.setComponent(resolveSanitizationComponent(useCase, task, originalField)); + return field; + } + + protected Component resolveSanitizationComponent(Case useCase, Task task, Field field) { + DataField dataField = useCase.getDataField(field.getStringId()); + if (dataField == null) { + return field.getComponent(); + } + + if (task != null && dataField.getDataRefComponents() != null) { + Component dataRefComponent = dataField.getDataRefComponents().get(task.getTransitionId()); + if (dataRefComponent != null) { + return dataRefComponent; + } + } + + if (dataField.getComponent() != null) { + return dataField.getComponent(); + } + + return field.getComponent(); + } +} \ No newline at end of file From 26e17b49511d8b3f01179cd228f84826caa02995 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 15 Apr 2026 12:56:47 +0200 Subject: [PATCH 3/5] Update error message assertions in FieldSanitizationServiceTest Revised test assertions to match the updated error message format. This ensures consistency with the new message structure indicating the configured action for unsafe content. --- .../workflow/service/FieldSanitizationServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java index fda41288c40..5c11da16a16 100644 --- a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java @@ -214,7 +214,7 @@ void shouldThrowExceptionWhenActionIsRejectAndPlainTextModeChangesContent() { () -> service.sanitize(input, field) ); - assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + assertEquals("Field [null] contains unsafe content and the configured action is REJECT.", exception.getMessage()); } @Test @@ -238,7 +238,7 @@ void shouldThrowExceptionWhenActionIsRejectAndSafeHtmlModeChangesContent() { () -> service.sanitize(input, field) ); - assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + assertEquals("Field [null] contains unsafe content and the configured action is REJECT.", exception.getMessage()); } @Test @@ -291,7 +291,7 @@ void shouldResolveActionCaseInsensitively() { () -> service.sanitize(input, field) ); - assertEquals("Unsafe content detected in field [null]", exception.getMessage()); + assertEquals("Field [null] contains unsafe content and the configured action is REJECT.", exception.getMessage()); } @Test From ff4679251a9b82c98c4e6ea0880ae6de806ff88d Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 15 Apr 2026 15:38:45 +0200 Subject: [PATCH 4/5] Update default sanitization mode to DISABLE_JAVASCRIPT Changed the default fallback sanitization mode from PLAIN_TEXT to DISABLE_JAVASCRIPT in SanitizationMode and FieldSanitizationService. This ensures better protection against potential JavaScript-related vulnerabilities. --- .../service/sanitization/FieldSanitizationService.java | 6 +++--- .../workflow/service/sanitization/SanitizationMode.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java index 10f39aab0f6..1071a8b0cee 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/FieldSanitizationService.java @@ -116,11 +116,11 @@ protected PolicyFactory resolvePolicy(SanitizationMode mode) { return SAFE_HTML_NO_LINKS_POLICY; case SAFE_HTML_RELAXED: return SAFE_HTML_RELAXED_POLICY; - case DISABLE_JAVASCRIPT: - return DISABLE_JAVASCRIPT_POLICY; case PLAIN_TEXT: - default: return PLAIN_TEXT_POLICY; + case DISABLE_JAVASCRIPT: + default: + return DISABLE_JAVASCRIPT_POLICY; } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java index d389a1ef14d..2a8e7b27454 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java @@ -13,7 +13,7 @@ public enum SanitizationMode { public static SanitizationMode from(String value) { if (value == null || value.isBlank()) { - return PLAIN_TEXT; + return DISABLE_JAVASCRIPT; } for (SanitizationMode mode : values()) { @@ -22,6 +22,6 @@ public static SanitizationMode from(String value) { } } - return PLAIN_TEXT; + return DISABLE_JAVASCRIPT; } } \ No newline at end of file From 569634b3c33124fadc099d475142d606e6ea1b57 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Wed, 15 Apr 2026 16:14:20 +0200 Subject: [PATCH 5/5] Refactor sanitization behavior to default to 'OFF' mode. Updated the default sanitization mode from 'DISABLE_JAVASCRIPT' to 'OFF' to align with behavior changes. Removed redundant test cases that validated HTML stripping in 'PLAIN_TEXT' mode, as it is no longer relevant. Adjusted existing tests to match the new default mode behavior. --- .../sanitization/SanitizationMode.java | 4 +-- .../FieldSanitizationIntegrationTest.java | 10 ------- .../service/FieldSanitizationServiceTest.java | 29 ++++--------------- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java index 2a8e7b27454..26e409313f2 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/sanitization/SanitizationMode.java @@ -13,7 +13,7 @@ public enum SanitizationMode { public static SanitizationMode from(String value) { if (value == null || value.isBlank()) { - return DISABLE_JAVASCRIPT; + return OFF; } for (SanitizationMode mode : values()) { @@ -22,6 +22,6 @@ public static SanitizationMode from(String value) { } } - return DISABLE_JAVASCRIPT; + return OFF; } } \ No newline at end of file diff --git a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java index 19746657374..259a6691152 100644 --- a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java +++ b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationIntegrationTest.java @@ -94,16 +94,6 @@ void offModeShouldKeepPlainTextUntouched() throws Exception { assertStoredEquals("text_off", PAYLOAD_PLAIN, PAYLOAD_PLAIN); } - @Test - void plainDefaultShouldStripHtmlFromScriptPayload() throws Exception { - assertStoredEquals("text_plain_default", PAYLOAD_SCRIPT_BOLD, "Hello"); - } - - @Test - void plainDefaultShouldRemoveImgPayload() throws Exception { - assertStoredEquals("text_plain_default", PAYLOAD_IMG_ONERROR, ""); - } - @Test void plainDefaultShouldKeepPlainTextUntouched() throws Exception { assertStoredEquals("text_plain_default", PAYLOAD_PLAIN, PAYLOAD_PLAIN); diff --git a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java index 5c11da16a16..c22524481ee 100644 --- a/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/workflow/service/FieldSanitizationServiceTest.java @@ -45,23 +45,6 @@ void shouldReturnSameValueWhenInputIsPlainText() { assertEquals("Hello world 123", result); } - @Test - void shouldStripHtmlTagsWhenDefaultModeIsPlainText() { - TextField field = new TextField(); - String input = "Hello world"; - String result = service.sanitize(input, field); - assertEquals("Hello world", result); - assertNotEquals(input, result); - } - - @Test - void shouldStripDangerousAttributesWhenDefaultModeIsPlainText() { - TextField field = new TextField(); - String input = "test"; - String result = service.sanitize(input, field); - assertEquals("test", result); - } - @Test void shouldNotSanitizeWhenModeIsOff() { TextField field = new TextField(); @@ -93,34 +76,34 @@ void shouldResolveModeCaseInsensitively() { } @Test - void shouldUsePlainTextModeWhenComponentIsNull() { + void shouldUseOffModeWhenComponentIsNull() { TextField field = new TextField(); field.setComponent(null); String input = "Hello"; String result = service.sanitize(input, field); - assertEquals("Hello", result); + assertEquals("Hello", result); } @Test - void shouldUsePlainTextModeWhenComponentPropertiesAreNull() { + void shouldUseOffModeWhenComponentPropertiesAreNull() { TextField field = new TextField(); Component component = Mockito.mock(Component.class); when(component.getProperties()).thenReturn(null); field.setComponent(component); String input = "Hello"; String result = service.sanitize(input, field); - assertEquals("Hello", result); + assertEquals("Hello", result); } @Test - void shouldUsePlainTextModeWhenSanitizationModePropertyIsMissing() { + void shouldUseOffModeWhenSanitizationModePropertyIsMissing() { TextField field = new TextField(); Component component = Mockito.mock(Component.class); when(component.getProperties()).thenReturn(Map.of("otherKey", "true")); field.setComponent(component); String input = "text"; String result = service.sanitize(input, field); - assertEquals("text", result); + assertEquals("text", result); } @Test