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
@@ -0,0 +1,174 @@
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;
import java.util.Set;

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<String> 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<String> 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()));

NodeList nodes = (NodeList) XPathFactory.newInstance().newXPath()
.compile("//*[local-name()='setBody']/*[local-name()='groovy']")
.evaluate(doc, XPathConstants.NODESET);

List<String> 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<String> extractFromJson(String json) throws Exception {
List<String> scripts = new ArrayList<>();
collectFromJsonNode(mapper.readTree(json), scripts);
return scripts;
}

private static void collectFromJsonNode(JsonNode node, List<String> 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<String> extractFromYaml(String yaml) {
List<String> scripts = new ArrayList<>();
Object parsed = new Yaml().load(yaml);
collectFromYamlObject(parsed, scripts);
return scripts;
}

@SuppressWarnings("unchecked")
private static void collectFromYamlObject(Object obj, List<String> scripts) {
if (obj instanceof Map) {
Map<String, Object> map = (Map<String, Object>) obj;
if (map.containsKey(SCRIPT) && map.get(SCRIPT) instanceof Map) {
Map<String, Object> scriptBlock = (Map<String, Object>) 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 {

private static final Map<String, Set<String>> FORBIDDEN_STATIC_CALLS = Map.of(
"java.lang.System", Set.of("exit"),
"java.util.TimeZone", Set.of("setDefault")
);

private static final Set<String> FORBIDDEN_METHOD_NAMES = Set.of("getClass", "class");

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() {

@Override
public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
checkForbiddenCall(call.getOwnerType().getName(), call.getMethod());
super.visitStaticMethodCallExpression(call);
}

@Override
public void visitMethodCallExpression(MethodCallExpression call) {
String name = call.getMethodAsString();
if (FORBIDDEN_METHOD_NAMES.contains(name)) {
throw new SecurityException("Sandbox Denial: Reflection is forbidden.");
}
checkForbiddenCall(call.getObjectExpression().getText(), name);
super.visitMethodCallExpression(call);
}
})
);
}

private static void checkForbiddenCall(String receiver, String method) {
Set<String> forbidden = FORBIDDEN_STATIC_CALLS.get(receiver);
if (forbidden != null && forbidden.contains(method)) {
throw new SecurityException("Sandbox Denial: " + receiver + "." + method + "() is not allowed.");
}
}
}
}
35 changes: 7 additions & 28 deletions dil/src/main/java/org/assimbly/dil/validation/ScriptValidator.java
Original file line number Diff line number Diff line change
@@ -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());
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,6 +87,8 @@ public void removeFlowConfigurationIfExist(TreeMap<String,String> 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);
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down