Repository navigation
[cherry-pick][6.10] CDAP-21270: Enable JEXL expressions allowlisting in Wrangler Directives- #1040 - #1044
[cherry-pick][6.10] CDAP-21270: Enable JEXL expressions allowlisting in Wrangler Directives- #1040#1044sahusanket wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a JEXL sandbox allowlist feature to restrict the classes, methods, and properties executed in JEXL expressions. The review feedback highlights several critical issues that must be addressed: compilation errors in EL.java due to incorrect usage of the JEXL 3 Permissions API, a bootstrapping bug in ConfigStore.java that prevents existing installations from receiving the default allowlist, missing essential utility and collection classes in the default allowlist, an overly restrictive validation in JexlAllowlist.java that blocks method-only or property-only rules, and a potential NullPointerException in DirectivesHandler.java.
|
|
||
| private static void applyMethodPermissions(JexlAllowlist rule, JexlSandbox.Permissions permissions) { | ||
| if (!rule.isAllMethods()) { | ||
| rule.getMethods().forEach(method -> permissions.execute(method)); |
There was a problem hiding this comment.
In Apache Commons JEXL 3, JexlSandbox.Permissions does not have an execute(String) method. Instead, execute() returns a Names object, on which you must call add(String) to allow a method. Calling permissions.execute(method) will cause a compilation error. Please update it to use permissions.execute().add(method).
| rule.getMethods().forEach(method -> permissions.execute(method)); | |
| rule.getMethods().forEach(method -> permissions.execute().add(method)); |
| rule.getProperties().forEach(property -> { | ||
| permissions.read(property); | ||
| permissions.write(property); | ||
| }); |
There was a problem hiding this comment.
Similarly, JexlSandbox.Permissions does not have read(String) or write(String) methods. You must call read().add(property) and write().add(property) to allow property access. Calling them with arguments will cause a compilation error.
| rule.getProperties().forEach(property -> { | |
| permissions.read(property); | |
| permissions.write(property); | |
| }); | |
| rule.getProperties().forEach(property -> { | |
| permissions.read().add(property); | |
| permissions.write().add(property); | |
| }); |
| public void initialize() throws IOException { | ||
| DirectiveConfig config = getConfig(); | ||
|
|
||
| if (config != null) { | ||
| LOG.info("Directive config already exists"); | ||
| return; | ||
| } | ||
|
|
||
| LOG.info("Initializing Directive config with default values"); | ||
| JsonObject configJson = new JsonObject(); | ||
| configJson.add(DirectiveConfig.JEXL_ALLOWLIST_KEY, GSON.toJsonTree(DefaultJexlAllowlist.get())); | ||
| updateConfig(GSON.fromJson(configJson, DirectiveConfig.class)); | ||
| } |
There was a problem hiding this comment.
The initialize() method only bootstraps the default JEXL allowlist if no configuration exists in the database (config == null). For existing installations, config will not be null, meaning the default JEXL allowlist will never be initialized. If the JEXL allowlist feature is enabled, all JEXL expressions will fail because the allowlist is empty. Please update the initialization logic to check if config.getJexlAllowlist() == null and, if so, initialize it with the default values.
public void initialize() throws IOException {
DirectiveConfig config = getConfig();
if (config == null) {
LOG.info("Initializing Directive config with default values");
JsonObject configJson = new JsonObject();
configJson.add(DirectiveConfig.JEXL_ALLOWLIST_KEY, GSON.toJsonTree(DefaultJexlAllowlist.get()));
updateConfig(GSON.fromJson(configJson, DirectiveConfig.class));
return;
}
if (config.getJexlAllowlist() == null) {
LOG.info("Existing Directive config found without JEXL allowlist. Initializing JEXL allowlist with default values");
JsonObject configJson = config.toJson().getAsJsonObject();
configJson.add(DirectiveConfig.JEXL_ALLOWLIST_KEY, GSON.toJsonTree(DefaultJexlAllowlist.get()));
updateConfig(GSON.fromJson(configJson, DirectiveConfig.class));
} else {
LOG.info("Directive config already exists with JEXL allowlist");
}
}| private static final List<String> DEFAULT_CLASSES = Arrays.asList( | ||
| // Data types | ||
| "java.lang.Boolean", "java.lang.Byte", "java.lang.Character", "java.lang.Double", "java.lang.Float", | ||
| "java.lang.Integer", "java.lang.Long", "java.lang.Short", | ||
|
|
||
| // Strings | ||
| "java.lang.String", "java.lang.StringBuilder", "java.util.StringJoiner", | ||
|
|
||
| // Math | ||
| "java.lang.Math", "java.math.BigDecimal", "java.math.BigInteger", | ||
|
|
||
| // Time | ||
| "java.time.ZonedDateTime", "java.time.LocalDate", "java.time.LocalDateTime", "java.time.Instant", | ||
| "java.time.Duration", "java.time.format.DateTimeFormatter", | ||
|
|
||
| // Utilities | ||
| "java.util.Arrays", "java.util.Collections", "java.util.UUID", "java.util.Base64"); |
There was a problem hiding this comment.
The default JEXL allowlist is missing essential classes for standard Wrangler functions and collection operations. For example, standard namespaces like string (resolving to org.apache.commons.lang.StringUtils), json (resolving to io.cdap.functions.JsonFunctions), and standard collection classes like java.util.Map, java.util.HashMap, java.util.List, java.util.ArrayList are not included. When the JEXL allowlist is enabled, any expression using these namespaces or collections will fail. Please consider adding these classes to the default allowlist.
| private static List<String> sanitizeList(List<String> list, String type) { | ||
| if (list == null || list.isEmpty()) { | ||
| throw new IllegalArgumentException("The " + type + " list cannot be null or empty."); | ||
| } |
There was a problem hiding this comment.
The sanitizeList method throws an IllegalArgumentException if the list is empty. However, it is a very common and valid use case to allow a class with only methods and no properties (or vice versa). Forcing both lists to be non-empty prevents users from defining secure, method-only or property-only allowlist rules. Consider allowing empty lists and defaulting null lists to an empty list.
| private static List<String> sanitizeList(List<String> list, String type) { | |
| if (list == null || list.isEmpty()) { | |
| throw new IllegalArgumentException("The " + type + " list cannot be null or empty."); | |
| } | |
| private static List<String> sanitizeList(List<String> list, String type) { | |
| if (list == null) { | |
| return Collections.emptyList(); | |
| } |
| if (!jexlAllowlistEnabled) { | ||
| DirectiveConfig existingConfig = configStore.getConfig(); | ||
| JsonObject existingJson = existingConfig.toJson().getAsJsonObject(); | ||
| JsonObject newJson = config.toJson().getAsJsonObject(); | ||
| existingJson.add(DirectiveConfig.EXCLUSIONS_KEY, newJson.get(DirectiveConfig.EXCLUSIONS_KEY)); | ||
| existingJson.add(DirectiveConfig.ALIASES_KEY, newJson.get(DirectiveConfig.ALIASES_KEY)); | ||
| config = GSON.fromJson(existingJson, DirectiveConfig.class); | ||
| } |
There was a problem hiding this comment.
If existingConfig is null (e.g., if the database has not been initialized or the row was deleted), calling existingConfig.toJson() will throw a NullPointerException. Consider adding a null check for existingConfig before calling methods on it.
| if (!jexlAllowlistEnabled) { | |
| DirectiveConfig existingConfig = configStore.getConfig(); | |
| JsonObject existingJson = existingConfig.toJson().getAsJsonObject(); | |
| JsonObject newJson = config.toJson().getAsJsonObject(); | |
| existingJson.add(DirectiveConfig.EXCLUSIONS_KEY, newJson.get(DirectiveConfig.EXCLUSIONS_KEY)); | |
| existingJson.add(DirectiveConfig.ALIASES_KEY, newJson.get(DirectiveConfig.ALIASES_KEY)); | |
| config = GSON.fromJson(existingJson, DirectiveConfig.class); | |
| } | |
| if (!jexlAllowlistEnabled) { | |
| DirectiveConfig existingConfig = configStore.getConfig(); | |
| if (existingConfig != null) { | |
| JsonObject existingJson = existingConfig.toJson().getAsJsonObject(); | |
| JsonObject newJson = config.toJson().getAsJsonObject(); | |
| existingJson.add(DirectiveConfig.EXCLUSIONS_KEY, newJson.get(DirectiveConfig.EXCLUSIONS_KEY)); | |
| existingJson.add(DirectiveConfig.ALIASES_KEY, newJson.get(DirectiveConfig.ALIASES_KEY)); | |
| config = GSON.fromJson(existingJson, DirectiveConfig.class); | |
| } | |
| } |
original PR : #1040