From 1fd223e356107a075a82ae94518e093e7f747a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Mon, 18 May 2026 16:43:19 +0100 Subject: [PATCH 1/5] pre validation of groovy scripts during flow install when using the native Apache Camel Groovy engine (no sandbox required) --- .../GroovyScriptSecurityValidator.java | 176 ++++++++++++++++++ .../dil/validation/ScriptValidator.java | 35 +--- .../integration/impl/BaseIntegration.java | 6 + 3 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java diff --git a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java new file mode 100644 index 00000000..6a9cb3d4 --- /dev/null +++ b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java @@ -0,0 +1,176 @@ +package org.assimbly.dil.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; +import org.codehaus.groovy.classgen.GeneratorContext; +import org.codehaus.groovy.control.*; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.w3c.dom.*; +import org.yaml.snakeyaml.Yaml; + +import javax.xml.parsers.*; +import javax.xml.xpath.*; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class GroovyScriptSecurityValidator { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final String SCRIPT = "script"; + private static final String GROOVY = "groovy"; + + private GroovyScriptSecurityValidator() {} + + public static void validate(String mediaType, String configuration) throws Exception { + List scripts; + + if (mediaType.toLowerCase().contains("xml")) { + scripts = extractFromXml(configuration); + } else if (mediaType.toLowerCase().contains("json")) { + scripts = extractFromJson(configuration); + } else { + scripts = extractFromYaml(configuration); + } + + for (int i = 0; i < scripts.size(); i++) { + validateScript(scripts.get(i), i + 1); + } + } + + // ------------------------------------------------------------------------- + // Extractors + // ------------------------------------------------------------------------- + + private static List extractFromXml(String xml) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document doc = factory.newDocumentBuilder() + .parse(new ByteArrayInputStream(xml.getBytes())); + + NodeList nodes = (NodeList) XPathFactory.newInstance().newXPath() + .compile("//*[local-name()='script']/*[local-name()='groovy']") + .evaluate(doc, XPathConstants.NODESET); + + List scripts = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + String text = nodes.item(i).getTextContent().trim(); + if (!text.isBlank()) scripts.add(text); + } + return scripts; + } + + private static List extractFromJson(String json) throws Exception { + List scripts = new ArrayList<>(); + collectFromJsonNode(mapper.readTree(json), scripts); + return scripts; + } + + private static void collectFromJsonNode(JsonNode node, List scripts) { + if (node.isObject()) { + if (node.has(SCRIPT) && node.get(SCRIPT).has(GROOVY)) { + String text = node.get(SCRIPT).get(GROOVY).asText().trim(); + if (!text.isBlank()) scripts.add(text); + } + node.fields().forEachRemaining(entry -> collectFromJsonNode(entry.getValue(), scripts)); + } else if (node.isArray()) { + node.forEach(child -> collectFromJsonNode(child, scripts)); + } + } + + @SuppressWarnings("unchecked") + private static List extractFromYaml(String yaml) { + List scripts = new ArrayList<>(); + Object parsed = new Yaml().load(yaml); + collectFromYamlObject(parsed, scripts); + return scripts; + } + + @SuppressWarnings("unchecked") + private static void collectFromYamlObject(Object obj, List scripts) { + if (obj instanceof Map) { + Map map = (Map) obj; + if (map.containsKey(SCRIPT) && map.get(SCRIPT) instanceof Map) { + Map scriptBlock = (Map) map.get(SCRIPT); + if (scriptBlock.containsKey(GROOVY)) { + String text = String.valueOf(scriptBlock.get(GROOVY)).trim(); + if (!text.isBlank()) scripts.add(text); + } + } + map.values().forEach(v -> collectFromYamlObject(v, scripts)); + } else if (obj instanceof List) { + ((List) obj).forEach(item -> collectFromYamlObject(item, scripts)); + } + } + + // ------------------------------------------------------------------------- + // AST Validator + // ------------------------------------------------------------------------- + + public static void validateScript(String scriptText, int index) { + CompilerConfiguration config = new CompilerConfiguration(); + config.addCompilationCustomizers(new SecurityCheckCustomizer()); + + try { + new groovy.lang.GroovyShell(config).parse(scriptText); + } catch (Exception e) { + throw new SecurityException( + "Groovy script #" + index + " failed to parse for security reasons: " + e.getMessage(), e); + } + } + + private static class SecurityCheckCustomizer extends CompilationCustomizer { + + public SecurityCheckCustomizer() { + super(CompilePhase.SEMANTIC_ANALYSIS); + } + + @Override + public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) { + classNode.getMethods().forEach(method -> + method.getCode().visit(new CodeVisitorSupport() { + + // Catches fully-qualified static calls: java.lang.System.exit() + @Override + public void visitStaticMethodCallExpression(StaticMethodCallExpression call) { + String owner = call.getOwnerType().getName(); + String name = call.getMethod(); + + if ("java.lang.System".equals(owner) && "exit".equals(name)) { + throw new SecurityException("Sandbox Denial: System.exit() is not allowed."); + } + if ("java.util.TimeZone".equals(owner) && "setDefault".equals(name)) { + throw new SecurityException("Sandbox Denial: Cannot change global TimeZone."); + } + super.visitStaticMethodCallExpression(call); + } + + // Catches both instance/dynamic calls AND unqualified static calls + // e.g. TimeZone.setDefault(), System.exit(), obj.getClass() + @Override + public void visitMethodCallExpression(MethodCallExpression call) { + String name = call.getMethodAsString(); + String receiver = call.getObjectExpression().getText(); + + if ("getClass".equals(name) || "class".equals(name)) { + throw new SecurityException("Sandbox Denial: Reflection is forbidden."); + } + if ("java.lang.System".equals(receiver) && "exit".equals(name)) { + throw new SecurityException("Sandbox Denial: System.exit() is not allowed."); + } + if ("java.util.TimeZone".equals(receiver) && "setDefault".equals(name)) { + throw new SecurityException("Sandbox Denial: Cannot change global TimeZone."); + } + super.visitMethodCallExpression(call); + } + }) + ); + } + } +} \ No newline at end of file diff --git a/dil/src/main/java/org/assimbly/dil/validation/ScriptValidator.java b/dil/src/main/java/org/assimbly/dil/validation/ScriptValidator.java index 21ae6071..33775e20 100755 --- a/dil/src/main/java/org/assimbly/dil/validation/ScriptValidator.java +++ b/dil/src/main/java/org/assimbly/dil/validation/ScriptValidator.java @@ -1,20 +1,19 @@ package org.assimbly.dil.validation; import org.apache.camel.Exchange; -import org.apache.camel.RuntimeCamelException; +import org.apache.camel.Expression; import org.apache.camel.language.groovy.GroovyExpression; +import org.apache.camel.language.groovy.GroovyLanguage; import org.apache.camel.model.language.JavaScriptExpression; +import org.apache.camel.spi.Language; import org.assimbly.dil.validation.beans.script.EvaluationRequest; import org.assimbly.dil.validation.beans.script.EvaluationResponse; import org.assimbly.dil.validation.beans.script.ExchangeDto; import org.assimbly.dil.validation.beans.script.ScriptDto; import org.assimbly.dil.validation.scripts.ExchangeMarshaller; -import org.assimbly.sandbox.executors.GroovySandboxExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; - public class ScriptValidator { protected Logger log = LoggerFactory.getLogger(getClass()); @@ -55,30 +54,10 @@ public EvaluationResponse validate(EvaluationRequest evaluationRequest) { private EvaluationResponse validateStrictGroovyScript(ExchangeDto exchangeDto, String script) { try { - // 1. Unmarshall the test data - Exchange exchangeRequest = ExchangeMarshaller.unmarshall(exchangeDto); - - // 2. Use the dedicated Sandbox Executor logic - // We call a modified version or the same executor to ensure - // the ClassLoader isolation and SecurityManager are active. - GroovySandboxExecutor.execute(script, exchangeRequest); - - // 3. Marshall the result back - ExchangeDto exchangeDtoResponse = ExchangeMarshaller.marshall(exchangeRequest); - - // Use the script's body or a specific variable as the 'response' string - String scriptOutput = String.valueOf(exchangeRequest.getIn().getBody()); - - return createOKRequestResponse(exchangeDtoResponse, scriptOutput); - - } catch (SecurityException | RuntimeCamelException e) { - // This catches the BLACKLIST_PATTERN or SecurityManager violations - log.error("Sandbox Security Violation: ", e); - return createBadRequestResponse(exchangeDto, "Security Error: " + e.getMessage()); - } catch (org.codehaus.groovy.control.CompilationFailedException e) { - // This catches syntax errors - log.error("Groovy Syntax Error: ", e); - return createBadRequestResponse(exchangeDto, "Syntax Error: " + e.getMessage()); + // security validation + GroovyScriptSecurityValidator.validateScript(script, 0); + // execute + return validateGroovyScript(exchangeDto, script); } catch (Exception e) { log.error("Execution error during validation: ", e); return createBadRequestResponse(exchangeDto, "Execution Error: " + e.getMessage()); diff --git a/integration/src/main/java/org/assimbly/integration/impl/BaseIntegration.java b/integration/src/main/java/org/assimbly/integration/impl/BaseIntegration.java index bf4787d9..37dc5d4d 100644 --- a/integration/src/main/java/org/assimbly/integration/impl/BaseIntegration.java +++ b/integration/src/main/java/org/assimbly/integration/impl/BaseIntegration.java @@ -6,6 +6,7 @@ import org.apache.camel.spi.EventNotifier; import org.assimbly.dil.model.FlowConfigurationResult; import org.assimbly.dil.transpiler.model.EndpointDefinition; +import org.assimbly.dil.validation.GroovyScriptSecurityValidator; import org.assimbly.docconverter.DocConverter; import org.assimbly.dil.validation.HttpsCertificateValidator; import org.assimbly.dil.validation.beans.Expression; @@ -86,6 +87,8 @@ public void removeFlowConfigurationIfExist(TreeMap configuration) public void setFlowConfiguration(String flowId, String mediaType, String configuration) throws Exception { try { + // Validate Groovy scripts before doing anything else + GroovyScriptSecurityValidator.validate(mediaType, configuration); if(mediaType.toLowerCase().contains("xml")) { flowConfigurationResult = convertXMLToFlowConfiguration(flowId, configuration); @@ -100,6 +103,9 @@ public void setFlowConfiguration(String flowId, String mediaType, String configu putFlowConfigurationToMap(flowId, mediaType, configuration); + } catch (SecurityException e) { + log.error("Flow configuration rejected: Groovy script failed security validation", e); + throw e; } catch (Exception e) { log.error("Set flow configuration failed",e); } From 16caaf80b1ec96092451f3e7a7ac44ac82019f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Mon, 18 May 2026 17:49:05 +0100 Subject: [PATCH 2/5] code refactoring --- .../GroovyScriptSecurityValidator.java | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java index 6a9cb3d4..ad82b834 100644 --- a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java +++ b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; public class GroovyScriptSecurityValidator { @@ -127,6 +128,13 @@ public static void validateScript(String scriptText, int index) { private static class SecurityCheckCustomizer extends CompilationCustomizer { + private static final Map> FORBIDDEN_STATIC_CALLS = Map.of( + "java.lang.System", Set.of("exit"), + "java.util.TimeZone", Set.of("setDefault") + ); + + private static final Set FORBIDDEN_METHOD_NAMES = Set.of("getClass", "class"); + public SecurityCheckCustomizer() { super(CompilePhase.SEMANTIC_ANALYSIS); } @@ -136,41 +144,30 @@ public void call(SourceUnit source, GeneratorContext context, ClassNode classNod classNode.getMethods().forEach(method -> method.getCode().visit(new CodeVisitorSupport() { - // Catches fully-qualified static calls: java.lang.System.exit() @Override public void visitStaticMethodCallExpression(StaticMethodCallExpression call) { - String owner = call.getOwnerType().getName(); - String name = call.getMethod(); - - if ("java.lang.System".equals(owner) && "exit".equals(name)) { - throw new SecurityException("Sandbox Denial: System.exit() is not allowed."); - } - if ("java.util.TimeZone".equals(owner) && "setDefault".equals(name)) { - throw new SecurityException("Sandbox Denial: Cannot change global TimeZone."); - } + checkForbiddenCall(call.getOwnerType().getName(), call.getMethod()); super.visitStaticMethodCallExpression(call); } - // Catches both instance/dynamic calls AND unqualified static calls - // e.g. TimeZone.setDefault(), System.exit(), obj.getClass() @Override public void visitMethodCallExpression(MethodCallExpression call) { - String name = call.getMethodAsString(); - String receiver = call.getObjectExpression().getText(); - - if ("getClass".equals(name) || "class".equals(name)) { + String name = call.getMethodAsString(); + if (FORBIDDEN_METHOD_NAMES.contains(name)) { throw new SecurityException("Sandbox Denial: Reflection is forbidden."); } - if ("java.lang.System".equals(receiver) && "exit".equals(name)) { - throw new SecurityException("Sandbox Denial: System.exit() is not allowed."); - } - if ("java.util.TimeZone".equals(receiver) && "setDefault".equals(name)) { - throw new SecurityException("Sandbox Denial: Cannot change global TimeZone."); - } + checkForbiddenCall(call.getObjectExpression().getText(), name); super.visitMethodCallExpression(call); } }) ); } + + private static void checkForbiddenCall(String receiver, String method) { + Set forbidden = FORBIDDEN_STATIC_CALLS.get(receiver); + if (forbidden != null && forbidden.contains(method)) { + throw new SecurityException("Sandbox Denial: " + receiver + "." + method + "() is not allowed."); + } + } } } \ No newline at end of file From fdd3d00c3e4bcf30e078b1b78d2d25ceef7c93d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Mon, 18 May 2026 18:30:33 +0100 Subject: [PATCH 3/5] resolved codacy critical warning - DOCTYPE declarations are enabled for this DocumentBuilderFactory --- .../assimbly/dil/validation/GroovyScriptSecurityValidator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java index ad82b834..549f835b 100644 --- a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java +++ b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java @@ -52,6 +52,7 @@ public static void validate(String mediaType, String configuration) throws Excep private static List extractFromXml(String xml) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // disabling DOCTYPE - Classic XXE (XML External Entity) vulnerability warning Document doc = factory.newDocumentBuilder() .parse(new ByteArrayInputStream(xml.getBytes())); From 28c3e7f9f12410f66d3dd350b60e7593d9a69db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Tue, 19 May 2026 11:44:28 +0100 Subject: [PATCH 4/5] return error report if a SecurityException is thrown --- .../org/assimbly/integration/impl/CamelIntegration.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration/src/main/java/org/assimbly/integration/impl/CamelIntegration.java b/integration/src/main/java/org/assimbly/integration/impl/CamelIntegration.java index e27b0566..5b622a52 100644 --- a/integration/src/main/java/org/assimbly/integration/impl/CamelIntegration.java +++ b/integration/src/main/java/org/assimbly/integration/impl/CamelIntegration.java @@ -1541,8 +1541,13 @@ public String configureAndRestartFlow(String flowId, long timeout, String mediaT } public String installFlow(String flowId, long timeout, String mediaType, String configuration) throws Exception { - super.setFlowConfiguration(flowId, mediaType, configuration); - return startFlow(flowId, timeout); + try { + super.setFlowConfiguration(flowId, mediaType, configuration); + return startFlow(flowId, timeout); + } catch (SecurityException e) { + finishFlowActionReport(flowId, "error", e.getMessage(),"error"); + return loadReport; + } } public String uninstallFlow(String flowId, long timeout) throws Exception { From 27e9c4feeb7219455807e489a04161e37f27e327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Gon=C3=A7alves?= Date: Tue, 19 May 2026 13:23:39 +0100 Subject: [PATCH 5/5] use setBody instead of script, so that groovy result change the exchange body --- .../assimbly/dil/validation/GroovyScriptSecurityValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java index 549f835b..89405e09 100644 --- a/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java +++ b/dil/src/main/java/org/assimbly/dil/validation/GroovyScriptSecurityValidator.java @@ -57,7 +57,7 @@ private static List extractFromXml(String xml) throws Exception { .parse(new ByteArrayInputStream(xml.getBytes())); NodeList nodes = (NodeList) XPathFactory.newInstance().newXPath() - .compile("//*[local-name()='script']/*[local-name()='groovy']") + .compile("//*[local-name()='setBody']/*[local-name()='groovy']") .evaluate(doc, XPathConstants.NODESET); List scripts = new ArrayList<>();