Skip to content

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

Merged
riyaa14 merged 1 commit into
developfrom
feature/jexl-allowlisting
Sep 8, 2026
Merged

riyaa14 merged 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
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

Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/DirectiveConfig.java Outdated
Comment thread wrangler-storage/pom.xml Outdated
}

/** Custom GSON adapter for {@link DirectiveConfig}. */
public static final class DirectiveConfigDeserializer implements JsonDeserializer<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.

Move this to a separate file.

Comment thread wrangler-api/src/main/java/io/cdap/wrangler/api/JexlAllowlist.java Outdated
}
if (!jexlAllowlistEnabled && userAllowlist != null) {
throw new BadRequestException(
"Updates to JEXL allowlist in the wrangler directives config is disabled by administrator.");

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.

Simply use the error message suggested previously:

Updates to JEXL allowlist in the wrangler directives config is not allowed.

No need to mention that it is disabled by the administrator.

throw new BadRequestException(
"Updates to JEXL allowlist in the wrangler directives config is disabled by administrator.");
}
// No updates to JexlAllowlist when JEXL Allowlist feature is disabled

@vsethi09 vsethi09 Sep 7, 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.

Is this comment correct or misplaced?
Seems like this was checked above.

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.

Yes, this is correct

Comment on lines +1014 to +1020
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);

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.

What is this piece of code trying to do? Is there a better way to do so?

Can the parameterized constructor of DirectiveConfig with new getter functions be used?

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.

We are trying to persist jexlAllowlist as it is before the feature was disabled. When feature is disabled, users don't provide jexlAllowlist field, and because the API is not additive, the payload with only exclusions and aliases makes jexlAllowlist field null (it gets removed). To persist this value, we want to add exclusions and aliases to existing config containing jexlAllowlist rather than overriding jexlAllowlist to null.

jexlAllowlist in this case stays in database, it doesn't gets used until feature flag in enabled again and users get the same jexlAllowlist they configured before.

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch 2 times, most recently from e340705 to 3122ca8 Compare September 7, 2026 09:51
Comment on lines +39 to +41
public static final String CLASS_NAME_KEY = "className";
public static final String METHODS_KEY = "methods";
public static final String PROPERTIES_KEY = "properties";

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.

Does this need to be public?

Also, these same constants are defined in JexlAllowlist class. Can they be reused?

/**
* Custom GSON deserializer for {@link DirectiveConfig}.
*/
@Deprecated

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 annotation needed?

Why is new deprecated class being added?

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch 2 times, most recently from 7959721 to a9593b2 Compare September 8, 2026 12:39
* Custom GSON deserializer for {@link DirectiveConfig}.
*/
public final class DirectiveConfigDeserializer implements JsonDeserializer<DirectiveConfig> {
private static final Type SET_OF_STRING_TYPE = new TypeToken<Set<String>>() { }.getType();

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.

Standard Java and Google naming conventions favor concise, descriptive identifiers over verbose "spoken-out" prepositions like OF, TO, and redundant suffixes like TYPE.

Suggestions:

private static final Type STRING_SET_TYPE = new TypeToken<Set<String>>() {}.getType();
private static final Type STRING_MAP_TYPE = new TypeToken<Map<String, String>>() {}.getType();
private static final Type JEXL_ALLOWLIST_LIST_TYPE = new TypeToken<List<JexlAllowlist>>() {}.getType();

https://cdap.atlassian.net/wiki/spaces/CE/pages/1594098623/Java+Coding+Standards

Google Coding Standards

private static final String METHODS_KEY = "methods";
private static final String PROPERTIES_KEY = "properties";

private static final Type LIST_OF_STRING_TYPE = new TypeToken<List<String>>() { }.getType();

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.

Standard Java and Google naming conventions favor concise, descriptive identifiers over verbose "spoken-out" prepositions like OF, TO, and redundant suffixes like TYPE.

Suggestions:

private static final Type STRING_LIST_TYPE = new TypeToken<List<String>>() {}.getType();

https://cdap.atlassian.net/wiki/spaces/CE/pages/1594098623/Java+Coding+Standards

Google Coding Standards

Comment on lines +37 to +49
@Test
public void testDirectiveConfigDeserialization() {
String json = "{\n"
+ " \"exclusions\": [\"drop\"],\n"
+ " \"aliases\": {\"p\": \"parse-as-json\"},\n"
+ " \"jexlAllowlist\": [\n"
+ " {\n"
+ " \"className\": \"java.lang.String\",\n"
+ " \"methods\": [\"*\"],\n"
+ " \"properties\": [\"*\"]\n"
+ " }\n"
+ " ]\n"
+ "}";

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 one more test case with specific like of method and properties instead of * wildcard.

Comment on lines +102 to +103
String configStr = row.map(r -> r.getString(VAL_COL)).orElse(null);
return (configStr != null) ? GSON.fromJson(configStr, DirectiveConfig.class) : 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.

Using null checks with Java Optional<T> is bit of anti-pattern.

Simplify:

return row.map(r -> r.getString(VAL_COL))
          .map(str -> GSON.fromJson(str, DirectiveConfig.class))
          .orElse(null);

@riyaa14
riyaa14 force-pushed the feature/jexl-allowlisting branch from a9593b2 to e00c23d Compare September 8, 2026 14:20
@riyaa14
riyaa14 merged commit b0d9657 into develop Sep 8, 2026
10 of 11 checks passed
@riyaa14
riyaa14 deleted the feature/jexl-allowlisting branch September 8, 2026 14:34
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