Skip to content

CDAP-21270: Enable JEXL expressions allowlisting in Wrangler Directives - #1040

Open
riyaa14 wants to merge 1 commit into
developfrom
feature/jexl-allowlisting
Open

CDAP-21270: Enable JEXL expressions allowlisting in Wrangler Directives#1040
riyaa14 wants to merge 1 commit into
developfrom
feature/jexl-allowlisting

Conversation

@riyaa14

@riyaa14 riyaa14 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Context

This PR introduces a JEXL allowlist mechanism to restrict which Java classes and methods can be invoked within JEXL expressions evaluated by Wrangler directives (e.g., filter, set-column, send-to-error). This enhances the security of JEXL evaluations by preventing arbitrary code execution.

The allowlist enforcement is controlled via the Feature.WRANGLER_JEXL_ALLOWLIST feature flag.

Key Changes

API Additions:
Introduced JexlAllowlist, DefaultJexlAllowlist, and DirectiveJexlAllowlist in wrangler-api to define the schema and default rules for allowed JEXL accesses.

Expression Compilation:
Added CompileOptions to wrangler-core to pass allowlist context and restrictions during JEXL expression compilation in EL.java.

Config Store Initialization: Updated ConfigStore with an initialize() method to populate the store with a default directive config (including DefaultJexlAllowlist) if none exists. Added custom GSON deserializers for parsing JexlAllowlist.

Version Bump: JEXL 3.0 to 3.1, this is compatible change, needed because 3.0 didn't support allowlist mode, we would have need to define a exclusion and inclusion list.

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from ff086b3 to 5f9b323 Compare August 14, 2026 15:47
@riyaa14
riyaa14 requested review from sahusanket and vsethi09 August 14, 2026 15:47
@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from 5f9b323 to 67dcbb4 Compare August 14, 2026 15:49

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a secure JEXL execution sandbox in Wrangler to restrict allowed classes, methods, and properties, updating various directives to compile expressions with sandbox rules and adding a comprehensive performance benchmark. The review feedback identifies several critical improvement opportunities, primarily focusing on preventing potential NullPointerExceptions in EL.java and JexlInclusion.java (especially when handling null JexlInfo or deserialized null fields). Additionally, the feedback points out a logic bug where the sandbox is incorrectly enforced even when the secure JEXL feature is disabled, and advises against committing user-specific absolute paths in .vscode/settings.json.

I am having trouble creating individual review comments. Click here to see my feedback.

wrangler-core/src/main/java/io/cdap/wrangler/expression/EL.java (156-175)

high

The info object returned by jexlEx.getInfo() can be null in some JexlException scenarios. Accessing info.getDetail(), info.getLine(), or info.getColumn() without a null check will throw a NullPointerException. Please add a null check for info before accessing its properties, similar to how it is handled in handleExecutionException.

    if (ex instanceof JexlException) {
      JexlException jexlEx = (JexlException) ex;
      JexlInfo info = jexlEx.getInfo();
      String detail = (info == null || info.getDetail() == null) ? expression : info.getDetail().toString();
      int line = info == null ? 0 : info.getLine();
      int column = info == null ? 0 : info.getColumn();
      String errorMessage = jexlEx.getMessage();
      if (errorMessage != null && (errorMessage.contains("unsolvable function/method")
          || errorMessage.contains("unsolvable property"))) {
        return new ELException(
            String.format("Security violation: Access to JEXL component '%s' is not "
                    + "permitted by wrangler. Hence, this JEXL expression '%s' can't be resolved.",
                detail, expression),
            jexlEx);
      }
      return new ELException(
          String.format("Error encountered while compiling '%s' at line '%d' "
              + "and column '%d'. Make sure a valid jexl "
              + "transformation is provided.",
              detail, line, column),
          jexlEx);
    }

wrangler-core/src/main/java/io/cdap/wrangler/expression/EL.java (189-191)

high

If allowlistEnabled is false, the sandbox should not be created or enforced at all. However, the current condition !allowlistEnabled && (inclusions == null || inclusions.isEmpty()) means that if allowlistEnabled is false but inclusions is not empty, a sandbox will still be created and enforced. This violates the feature flag and can break existing JEXL expressions in pipelines where the secure JEXL feature is disabled. Please simplify the check to return null immediately if allowlistEnabled is false.

    if (!allowlistEnabled) {
      return null;
    }

.vscode/settings.json (3-13)

medium

Committing user-specific absolute paths (such as /usr/local/google/home/riyagarg/...) in shared configuration files is a bad practice. It will cause build or IDE configuration issues for other developers who do not have the same directory structure. Consider removing this file from the repository, adding .vscode/ to .gitignore, or using standard/relative paths.

wrangler-core/src/main/java/io/cdap/wrangler/expression/EL.java (215-217)

medium

If inclusions contains a null element, iterating over it and calling applyInclusionRule will result in a NullPointerException when calling rule.getClassName(). Adding a null check for rule prevents this potential crash.

    if (rule == null || rule.getClassName() == null || rule.getClassName().trim().isEmpty()) {
      return;
    }

wrangler-api/src/main/java/io/cdap/wrangler/api/JexlInclusion.java (86-96)

medium

If JexlInclusion is deserialized from JSON and the JSON explicitly contains "methods": null or "properties": null, the fields will be set to null (bypassing constructor defaults). Returning null from these getters can cause NullPointerExceptions downstream. Returning an empty list when they are null is much safer.

  public List<String> getMethods() {
    return methods == null ? Collections.emptyList() : methods;
  }

  /**
   * Gets the list of allowed properties.
   *
   * @return the allowed properties
   */
  public List<String> getProperties() {
    return properties == null ? Collections.emptyList() : properties;
  }

wrangler-api/src/main/java/io/cdap/wrangler/api/JexlInclusion.java (103-114)

medium

If methods or properties is null due to deserialization, calling methods.isEmpty() or properties.isEmpty() will throw a NullPointerException. Adding null checks here ensures robust and defensive execution.

  public boolean isAllMethods() {
    return methods == null || methods.isEmpty() || methods.contains("*");
  }

  /**
   * Checks if all properties are allowed.
   *
   * @return true if all properties are allowed
   */
  public boolean isAllProperties() {
    return properties == null || properties.isEmpty() || properties.contains("*");
  }

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch 3 times, most recently from c3a9400 to 3b324a7 Compare August 14, 2026 15:57
@riyaa14 riyaa14 added the build Triggers unit test build label Aug 14, 2026
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlist.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/JexlInclusion.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/JexlInclusion.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveContext.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/directives/row/SendToError.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/utils/JexlHelper.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/directives/row/Fail.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveContext.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveContext.java Outdated
Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/expression/JexlAllowedClasses.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/expression/JexlAllowedClasses.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/expression/JexlAllowedClasses.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/expression/EL.java Outdated
Comment thread wrangler-core/src/main/java/io/cdap/wrangler/expression/EL.java Outdated
@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch 2 times, most recently from 65c518b to f144a68 Compare August 26, 2026 07:42
@riyaa14 riyaa14 changed the title feature: Enable JEXL expressions allowlisting in Wrangler Directives CDAP-21270: Enable JEXL expressions allowlisting in Wrangler Directives Sep 2, 2026
@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from dcf8704 to c7e05eb Compare September 2, 2026 17:08
* @return the list of JEXL inclusions
*/
public List<JexlAllowlist> getJexlAllowlist() {
return Collections.unmodifiableList(jexlAllowlist);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When JSON string is deserialized to object, jexlAllowlist can get set to null.

In that case Collections.unmodifiableList(null) will throw NullPointerException.

Please handle this, if it is valid.

@@ -0,0 +1,61 @@
/*
* Copyright © 2024 Cask Data, Inc.

@vsethi09 vsethi09 Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please change to 2026 t/o for new .java files.

/**
* The fully qualified name of the class to include (e.g. java.lang.Math).
*/
private String className;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add @NotNull annotation.

/**
* The fully qualified name of the class to include (e.g. java.lang.Math).
*/
private String className;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can all the fields in this class be marked as final?

}

public JexlAllowlist(String className, List<String> methods, List<String> properties) {
if (className == null || !isValidClassName(className.trim())) {

@vsethi09 vsethi09 Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Simplify the code and remove unnecessary trim(). It is not useful as the string is already invalid.

See the previously suggested code.

Remove .trim() usage t/o.

}

private static List<String> sanitizeList(List<String> list, String type) {
if (list == null || list.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove .trim() and simply use Strings.isNullOrEmpty()

Fix t/o

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

simply use Strings.isNullOrEmpty()

This is list of strings and isNullOrEmpty method is available for a individual strings, not for List

@vsethi09 vsethi09 Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My bad this comment was meant for line: if (className == null || !isValidClassName(className.trim())) {

It can be changed to Strings.isNullOrEmpty(className)

}
List<String> sanitizedList = list.stream()
.map(item -> item == null ? "" : item.trim())
.filter(item -> !item.isEmpty())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove this check, if input is invalid throw exception.

Simplify code to remove this check below:

    if (sanitizedList.isEmpty()) {
      throw new IllegalArgumentException("The " + type + " list cannot be null or empty.");
    }

this.properties = sanitizeList(properties, "property");
}

private static List<String> sanitizeList(List<String> list, String type) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should return immutable / unmodifiable list so that it cannot be mutated by getter.

* @param allowlist the allowlist
* @return the sandbox
*/
public static JexlSandbox createSandbox(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(Optional suggestion) This EL class is doing more than expression language handling, i.e. creating JEXL engine and sanbox.

The logic for JEXL Engine and Sandbox creation can be moved to a separate class like JexlEngineFactory.create(...), which constructs engine with sandbox.


I understand this could be some change, so it can be done later and kept as it is for now.


@Override
public void initialize(SystemServiceContext context) {
public void initialize(SystemServiceContext context) throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

throws IOException to be specific for the caller to handle?


try {
new ConfigStore(context).initialize();
} catch (Exception e) {

@vsethi09 vsethi09 Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Catch specific exception - IOException

--

Also no need to catch IOException and re-throw it.

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from c7e05eb to 712d5a0 Compare September 2, 2026 19:17
/**
* The JEXL inclusions rules.
*/
private List<JexlAllowlist> jexlAllowlist = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can be final if not modified instead this class?

*/
public List<JexlAllowlist> getJexlAllowlist() {
if (jexlAllowlist == null) {
throw new IllegalArgumentException("JEXL allowlist cannot be null in DirectiveConfig.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IllegalArgumentException is not the correct exception as this function doesn't take any argument.

For simplicity you could do:

return jexlAllowlist == null ? List.of() : Collections.unmodifiableList(jexlAllowlist)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we shouldn't return empty string here.

Eg: When a user provided jexlAllowlist, but during JSON parsing, null returned, in this case if we don't return an error and rather pass an empty list, JexlSandbox will have no allowlisted classes and will block all, even though user provided an allowlist.

Whereas, if we throw an error, we easily understands the root cause.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update: we are accepting null here, and throwing error later in DirectivesHandler for nulls.

Comment on lines +53 to +57
public JexlAllowlist() {
this.className = "";
this.methods = null;
this.properties = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ideal way to handle this using GSON adapter for deserialization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented and tested

}
return item;
})
.collect(Collectors.toList()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use Collectors.toUnmodifiableList instead of wrapping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Collectors doesn't have toUnmodifiableList, we'll need to do this:

Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList
or
wrap with unmodifiable list as done currently.

wrapping with unmodifiable list seems more readable.

}

private static boolean isValidClassName(String className) {
if (className == null || className.isEmpty()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use String.isNullOrEmpty()

Fix t/o

return;
}

// This is one time bootstrap of ConfigStore to initialize default directive config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Move this comment to wrangler-storage/src/main/java/io/cdap/wrangler/dataset/workspace/ConfigStore.java

The callers will never know about this behavior if the comment is mentioned here.

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from 712d5a0 to 247d27e Compare September 4, 2026 07:43
private final Map<String, String> aliases;
private final List<JexlAllowlist> jexlAllowlist;

public DirectiveConfig() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why is this public?

public DirectiveConfig(@Nullable Set<String> exclusions,
@Nullable Map<String, String> aliases,
@Nullable List<JexlAllowlist> jexlAllowlist) {
this.exclusions = exclusions == null ? new HashSet<>() : new HashSet<>(exclusions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use unmodifiable / immutable.

DirectiveConfig config = getConfig();

if (config != null) {
LOG.info("Directive config already exists", (Throwable) null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this needed?

(Throwable) null

return;
}

LOG.info("Initializing Directive config with default values", (Throwable) null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this needed?

(Throwable) null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Triggers unit test build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants