Sitelet https://web.archive.org/web/20210217194900/https://github.com/github/codeql/pull/4965
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Query for detecting JEXL injections #4965

Open
wants to merge 15 commits into
base: main
from

Conversation

@artem-smotrakov
Copy link
Contributor

@artem-smotrakov artem-smotrakov commented Jan 17, 2021 •

Java Expression Language (JEXL) is a simple expression language provided by the Apache Commons JEXL library. If a JEXL expression is built using attacker-controlled data,
and then evaluated, then it may allow the attacker to run arbitrary code (CWE-094). Here are several examples of JEXL injections:

  • GHSL-2020-012 in Nexus Repository Manager
  • CVE-2020-1961 and CVE-2014-0111 in Apache Syncope

I'd like to propose a new experimental query that looks for potential JEXL injections:

  • The query covers both JEXL 2 and 3 versions (there are differences in the APIs).
  • The query covers both JEXL expressions and scripts.
  • Added a qhelp file and an example of vulnerable code.
  • Added tests.

Here are examples of a true-positives:

  1. https://lgtm.com/query/625950423675: detected a path in commons-jexl (that's a command-line tool for use in development/testing).
  2. traccar/traccar#4624: RCE in Traccar
@artem-smotrakov artem-smotrakov requested a review from github/codeql-java as a code owner Jan 17, 2021
- Added TaintedSpringRequestBody source
- Added returningTaintedDataFromBean() taint step
- Added tests
@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 17, 2021

I noticed that parameters annotated with several Spring annotations are not considered as a source of taint. For example:

@PostMapping("/request")
public ResponseEntity requestEndpoint(@RequestBody Data data) {

Here the data parameter is not currently considered tainted. Maybe I am missing something but adding RemoteFlowSource doesn't work. I wrote a few tests for it:

  • testWithSpringControllerThatEvaluatesJexlFromPathVariable
  • testWithSpringControllerThatEvaluatesJexlFromRequestBody

Then, I added a new source TaintedSpringRequestBody, and the tests passed.

I also thought that an application may take a complex object as an endpoint parameter. Currently, the taint is not propagated in this case. I added a new taint step returningTaintedDataFromBean that detects returning data from a tainted bean by calling one of its getters (please see the tests). I know that such a step may cause a lot of false-positives. However, some testing on real projects discovered true-positives so far. I am still looking into the results, maybe there are false-positives as well. I need some time to finish the analysis.

Copy link
Contributor

@smowton smowton left a comment •

Because there are a lot of taint-propagating methods here and a lot of duplicated code, how about we abbreviate the whole thing with something like:

class TaintPropagatingJexlMethodCall extends MethodAccess
  string methodName
  RefType instanceType
  Expr taintFromExpr

  TaintPropagatingJexlMethodCall() {
    exists(Method m | 
      this.getMethod() = m and 
      m.getDeclaringType() = instanceType and 
      m.hasName(methodName) |
      instanceType instanceof JexlEngine and methodName = "createScript" and taintFromExpr = this.getArgument(0) or
      instanceType instanceof JexlExpression and methodName = "callable" and taintFromExpr = this.getQualifier() or
      ...
  }

  predicate taintFlow(DataFlow::Node fromNode, DataFlow::Node toNode) {
    fromNode.asExpr() = taintFromExpr and toNode.asExpr() = this
  }
}

This sort of abbreviation won't suit all models, but I suspect it could get 464 lines of code down to 200 or so.

class TaintedSpringRequestBody extends DataFlow::Node {
TaintedSpringRequestBody() {
exists(SpringServletInputAnnotation a | this.asParameter().getAnAnnotation() = a)
}
}
Comment on lines 35 to 39

This comment has been minimized.

@smowton

smowton Jan 18, 2021
Contributor

I think the various Spring flow sources, which are included in RemoteFlowSource, should remove the need for this class (https://github.com/github/codeql/blob/main/java/ql/src/semmle/code/java/dataflow/FlowSources.qll#L110)

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 23, 2021 •
Author Contributor

(please also see my previous comment #4965 (comment))

At first, I used only RemoteFlowSource. I know that it contains various Spring flow sources. Then, I wrote several test cases for Spring, please see them in Jexl3Injection.lava:

  • testWithSpringControllerThatEvaluatesJexlFromPathVariable
  • testWithSpringControllerThatEvaluatesJexlFromRequestBody
  • testWithSpringControllerThatEvaluatesJexlFromRequestBodyWithNestedObjects

It turned out that those cases are not detected if only RemoteFlowSource is used. For example:

    @PostMapping("/request")
    public ResponseEntity testWithSpringControllerThatEvaluatesJexlFromPathVariable(
            @PathVariable String expr) {

        runJexlExpression(expr);
        return ResponseEntity.ok(HttpStatus.OK);
    }

The expr string comes from the URL path and therefore should be considered as a flow source. However, RemoteFlowSource doesn't seem to cover such cases. If I remove TaintedSpringRequestBody, then the tests above fail.

Maybe I missed some existing flow sources that cover such cases. If so, I think the at least should be included in RemoteFlowSource. Meanwhile, I'd like to keep TaintedSpringRequestBody. Maybe there is a better place for this class - please let me know. And please let me know if I am missing something.

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

That suggests these really are missing, but they don't relate particularly to JEXL -- please open a separate PR to add these.

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 25, 2021
Author Contributor

@smowton I agree, they don't relate to JEXL. If I remove TaintedSpringRequestBody now, then some tests will start failing. I'll then need to remove them as well. But I'd prefer to keep them. How about we keep TaintedSpringRequestBody here fore now? Once this pull request is accepted, I can open another one that moves TaintedSpringRequestBody to a better place. What do you think?

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

Sure, that order is ok

*/
predicate returningTaintedDataFromBean(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodAccess ma, Method m | ma.getMethod() = m |
m instanceof GetterMethod and

This comment has been minimized.

@smowton

smowton Jan 18, 2021
Contributor

I note we already have m instanceof GetterMethod and m.getDeclaringType() instanceof SpringUntrustedDataType in taintPreservingQualifierToMethod -- does that include the cases you need? Check if any of your tests fail without this?

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 23, 2021 •
Author Contributor

Yes, unfortunately, one of the tests fails without this (please see Jexl3Injection.java):

testWithSpringControllerThatEvaluatesJexlFromRequestBodyWithNestedObjects

This test covers the following case:

  • Tainted data comes from a complex bean CustomRequest that has @RequestBody annotation.
  • The CustomRequest class contains another bean of type Data.
    @PostMapping("/request")
    public ResponseEntity testWithSpringControllerThatEvaluatesJexlFromRequestBodyWithNestedObjects(
            @RequestBody CustomRequest customRequest) {

        String expr = customRequest.getData().getExpr();
        runJexlExpression(expr);

The TaintedSpringRequestBody flow source and returningTaintedDataFromBean step work together to catch this case.

I know that this step may be too broad so that it causes many false-positives (please see #4965 (comment)). I've been testing this on several codebases, so far it discovered some true-positives. I still need some time to finish the analysis. Taking into account that the query is experimental, I'd like to keep this taint propagation step. The impact of a JEXL injection is arbitrary code execution. For such a high impact, maybe it's okay to sacrifice the false-positive rate a bit in order to find more true-positives.

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 23, 2021
Author Contributor

TaintedSpringRequestBody and returningTaintedDataFromBean allowed to detect traccar/traccar#4624

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

Do you think the code to retrieve taint from a complex bean like that could be made universal? If so let's make a separate PR for that too -- if on the other hand you think it would be too noisy and should be restricted to this JEXL query then let's keep it as you say.

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 25, 2021
Author Contributor

I think it would be too noisy if we make it universal (at least with the current version of the step). Let's please keep it restricted to this JEXL query.

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 20, 2021

Because there are a lot of taint-propagating methods here and a lot of duplicated code, how about we abbreviate the whole thing with something like:

@smowton Thanks for the suggestion! I am still learning CodeQL and looking for ways to make the code shorter. I'll try to apply the suggestion.

@smowton
Copy link
Contributor

@smowton smowton commented Jan 21, 2021

Looks like some comments are applied and others not at the moment -- please ping me here when you're ready for another review

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 21, 2021

Looks like some comments are applied and others not at the moment -- please ping me here when you're ready for another review

Yeah, I didn't address some of them yet. Need some time. I'll let you know.

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 21, 2021

Thanks for the suggestion @intrigus-lgtm ! I forgot about <code> tag.

@intrigus-lgtm
Copy link
Contributor

@intrigus-lgtm intrigus-lgtm commented Jan 21, 2021

Thanks for the suggestion @intrigus-lgtm ! I forgot about <code> tag.

Also happens to me every now and then :)

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 23, 2021

@smowton Thanks for the review and suggestions! I've addressed your comments. I've also tried to simplify the code by applying your hints to the rest of the code. I would however still prefer using method/class definitions. I also make them a bit shorter.

Please also see my comments about TaintedSpringRequestBody flow source and returnsDataFromBean step:

Copy link
Contributor

@smowton smowton left a comment

Mostly looking good now, just one more major abbreviation we can make

* It supports both Jexl2 and Jexl3.
*/
class JexlInjectionConfig extends TaintTracking::Configuration {
TaintPropagatingJexlMethodCall taintPropagatingJexlMethodCall;

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

Suggested change
TaintPropagatingJexlMethodCall taintPropagatingJexlMethodCall;

Using this as a field generates multiple instances of the Configuration. I'm not sure the consequences of that, but they surely can't be good :)

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 25, 2021
Author Contributor

I didn't know about it, thanks! I'll apply this suggestion.

override predicate isSink(DataFlow::Node sink) { sink instanceof JexlEvaluationSink }

override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
taintPropagatingJexlMethodCall.taintFlow(fromNode, toNode) or

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

Suggested change
taintPropagatingJexlMethodCall.taintFlow(fromNode, toNode) or
any(TaintPropagatingJexlMethodCall c).taintFlow(fromNode, toNode) or
*/
private class JexlEvaluationSink extends DataFlow::ExprNode {
JexlEvaluationSink() {
exists(MethodAccess ma, Method m, Expr tainted | ma.getMethod() = m and tainted = asExpr() |

This comment has been minimized.

@smowton

smowton Jan 25, 2021 •
Contributor

Suggested change
exists(MethodAccess ma, Method m, Expr tainted | ma.getMethod() = m and tainted = asExpr() |
exists(MethodAccess ma, Method m, Expr taintFrom | ma.getMethod() = m and tainted = this.asExpr() |

(fairly arbitrary style preference in this repo: use explicit this)

Rename var to be slightly clearer about its role

/**
* Defines methods that triggers direct evaluation of Jexl expressions.
*/
abstract private class DirectJexlEvaluationMethod extends Method { }

/**
* A method in the `JexlExpression` class that evaluates a Jexl expression.
*/
private class JexlExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
JexlExpressionEvaluateMethod() {
getDeclaringType() instanceof JexlExpression and hasName("evaluate")
}
}

/**
* A method in the `JexlScript` class that executes a Jexl script.
*/
private class JexlScriptExecuteMethod extends DirectJexlEvaluationMethod {
JexlScriptExecuteMethod() { getDeclaringType() instanceof JexlScript and hasName("execute") }
}

/**
* A method in the `JxltEngine.Expression` class that evaluates an expression.
*/
private class JxltEngineExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
JxltEngineExpressionEvaluateMethod() {
getDeclaringType() instanceof JxltEngineExpression and hasName("evaluate")
}
}

/**
* A method in the `JxltEngine.Expression` class that evaluates the immediate sub-expressions.
*/
private class JxltEngineExpressionPrepareMethod extends DirectJexlEvaluationMethod {
JxltEngineExpressionPrepareMethod() {
getDeclaringType() instanceof JxltEngineExpression and hasName("prepare")
}
}

/**
* A method in the `JxltEngine.Template` class that evaluates a template.
*/
private class JxltEngineTemplateEvaluateMethod extends DirectJexlEvaluationMethod {
JxltEngineTemplateEvaluateMethod() {
getDeclaringType() instanceof JxltEngineTemplate and hasName("evaluate")
}
}

/**
* A method in the `UnifiedJEXL.Expression` class that evaluates a template.
*/
private class UnifiedJexlExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
UnifiedJexlExpressionEvaluateMethod() {
getDeclaringType() instanceof UnifiedJexlExpression and hasName("evaluate")
}
}

/**
* A method in the `UnifiedJEXL.Expression` class that evaluates the immediate sub-expressions.
*/
private class UnifiedJexlExpressionPrepareMethod extends DirectJexlEvaluationMethod {
UnifiedJexlExpressionPrepareMethod() {
getDeclaringType() instanceof UnifiedJexlExpression and hasName("prepare")
}
}

/**
* A method in the `UnifiedJEXL.Template` class that evaluates a template.
*/
private class UnifiedJexlTemplateEvaluateMethod extends DirectJexlEvaluationMethod {
UnifiedJexlTemplateEvaluateMethod() {
getDeclaringType() instanceof UnifiedJexlTemplate and hasName("evaluate")
}
}
Comment on lines 117 to 190

This comment has been minimized.

@smowton

smowton Jan 25, 2021
Contributor

Suggested change
/**
* Defines methods that triggers direct evaluation of Jexl expressions.
*/
abstract private class DirectJexlEvaluationMethod extends Method { }
/**
* A method in the `JexlExpression` class that evaluates a Jexl expression.
*/
private class JexlExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
JexlExpressionEvaluateMethod() {
getDeclaringType() instanceof JexlExpression and hasName("evaluate")
}
}
/**
* A method in the `JexlScript` class that executes a Jexl script.
*/
private class JexlScriptExecuteMethod extends DirectJexlEvaluationMethod {
JexlScriptExecuteMethod() { getDeclaringType() instanceof JexlScript and hasName("execute") }
}
/**
* A method in the `JxltEngine.Expression` class that evaluates an expression.
*/
private class JxltEngineExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
JxltEngineExpressionEvaluateMethod() {
getDeclaringType() instanceof JxltEngineExpression and hasName("evaluate")
}
}
/**
* A method in the `JxltEngine.Expression` class that evaluates the immediate sub-expressions.
*/
private class JxltEngineExpressionPrepareMethod extends DirectJexlEvaluationMethod {
JxltEngineExpressionPrepareMethod() {
getDeclaringType() instanceof JxltEngineExpression and hasName("prepare")
}
}
/**
* A method in the `JxltEngine.Template` class that evaluates a template.
*/
private class JxltEngineTemplateEvaluateMethod extends DirectJexlEvaluationMethod {
JxltEngineTemplateEvaluateMethod() {
getDeclaringType() instanceof JxltEngineTemplate and hasName("evaluate")
}
}
/**
* A method in the `UnifiedJEXL.Expression` class that evaluates a template.
*/
private class UnifiedJexlExpressionEvaluateMethod extends DirectJexlEvaluationMethod {
UnifiedJexlExpressionEvaluateMethod() {
getDeclaringType() instanceof UnifiedJexlExpression and hasName("evaluate")
}
}
/**
* A method in the `UnifiedJEXL.Expression` class that evaluates the immediate sub-expressions.
*/
private class UnifiedJexlExpressionPrepareMethod extends DirectJexlEvaluationMethod {
UnifiedJexlExpressionPrepareMethod() {
getDeclaringType() instanceof UnifiedJexlExpression and hasName("prepare")
}
}
/**
* A method in the `UnifiedJEXL.Template` class that evaluates a template.
*/
private class UnifiedJexlTemplateEvaluateMethod extends DirectJexlEvaluationMethod {
UnifiedJexlTemplateEvaluateMethod() {
getDeclaringType() instanceof UnifiedJexlTemplate and hasName("evaluate")
}
}
/**
* Defines methods that triggers direct evaluation of Jexl expressions.
*/
private class DirectJexlEvaluationMethod extends Method {
DirectJexlEvaluationMethod() {
getDeclaringType() instanceof JexlExpression and hasName("evaluate") or
getDeclaringType() instanceof JexlScript and hasName("execute") or
getDeclaringType() instanceof JxltEngineExpression and hasName(["evaluate", "prepare"]) or
getDeclaringType() instanceof JxltEngineTemplate and hasName("evaluate") or
getDeclaringType() instanceof UnifiedJexlExpression and hasName(["evaluate", "prepare"]) or
getDeclaringType() instanceof UnifiedJexlTemplate and hasName("evaluate")
}
}

Haven't checked, but if the other types lack a prepare method altogether then this could becomes just a list of 6 types and hasName(["evaluate", "prepare"])

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Jan 25, 2021
Author Contributor

Thanks for the suggestion - that makes it much shorter! Technically, I can write the following even if the types don't have all three methods:

private class DirectJexlEvaluationMethod extends Method {
  DirectJexlEvaluationMethod() {
    (
      getDeclaringType() instanceof JexlExpression
      or
      getDeclaringType() instanceof JexlScript
      or
      getDeclaringType() instanceof JxltEngineExpression
      or
      getDeclaringType() instanceof JxltEngineTemplate
      or
      getDeclaringType() instanceof UnifiedJexlExpression
      or
      getDeclaringType() instanceof UnifiedJexlTemplate
    ) and
    hasName(["evaluate", "execute", "prepare"])
  }
}

That would work. However, that looks a bit confusing to me since none of the listed types has all three methods. I'd prefer to use your version that describes the methods of the specific classes. Let me know if I am missing something.

- Merged multiple method definitions to DirectJexlEvaluationMethod
- Don't use TaintPropagatingJexlMethodCall field in JexlInjectionConfig
- Better variable names in JexlEvaluationSink
@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 25, 2021

@smowton I've applied your suggestions and formatted the code. Please have a look.

@smowton
Copy link
Contributor

@smowton smowton commented Jan 25, 2021

Great, this is looking good! Are you applying to the bounty program with this PR?

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 25, 2021

Great, this is looking good! Are you applying to the bounty program with this PR?

Thanks for the review! Yes, I am planning to apply to the bounty program once this PR is merged.

@smowton
Copy link
Contributor

@smowton smowton commented Jan 25, 2021

You should actually make your application now -- then the security lab folks will help evaluate the quality of the results and perhaps make more suggestions here.

@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Jan 25, 2021

Sure, created github/securitylab#249

ma.getAnArgument().getType() instanceof TypeString and
ma.getAnArgument() = taintFrom
Comment on lines 50 to 51

This comment has been minimized.

@pwntester

pwntester Feb 9, 2021
Contributor

I think you need to check that both arguments have the same index /cc @smowton

This comment has been minimized.

@smowton

smowton Feb 9, 2021
Contributor

Doh, yes, you could check taintFrom.getType() to make sure you're referring to the same arg

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Feb 10, 2021
Author Contributor

I've updated the query, thanks!

|
m instanceof DirectJexlEvaluationMethod and ma.getQualifier() = taintFrom
or
m instanceof CallableCallMethod and ma.getQualifier() = taintFrom

This comment has been minimized.

@pwntester

pwntester Feb 9, 2021
Contributor

This is matching all calls to java.util.concurrent.Callable no matter if they are created from a JEXL script or not which is adding quite a few false positives

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Feb 10, 2021
Author Contributor

To produce an alert, a Callable has to be tainted. This sink works together with TaintPropagatingJexlMethodCall that can make a Callable tainted if it is created from a tainted JexlExpression or JexlScript. There is no other way how a Callable can become tainted in this query. The sink is private, therefore it can't affect other queries. While running the query on multiple codebases, I didn't notice such a false positive. Please let me know if I am missing something. If you have a code snippet that results to a false positive, please let me know. I'll try to fix the query and add it as a test.

This comment has been minimized.

@pwntester

pwntester Feb 11, 2021
Contributor

Sure, you can use apache/groovy as test case. The project does not use JEXL and still gets 38 alerts. The problem arises when a Callable gets tainted. For example below obj is tainted and gets casted to groovy's Closure which implements the Callable interface:

Closure c = (Closure) ((Object[]) obj)[0];
..
c.call((Object)null);

It may be enough to set the sink in the callable() qualifier assuming that it will get executed at some point, maybe stored in an object field and then reached by a different flow.

This comment has been minimized.

@artem-smotrakov

artem-smotrakov Feb 11, 2021
Author Contributor

You're right, good catch!

It may be enough to set the sink in the callable() qualifier assuming that it will get executed at some point, maybe stored in an object field and then reached by a different flow.

I've updated the query with your suggestion. Now it doesn't show false positives for Apache Groovy. Thanks!

- Added a dataflow config to track setting a sandbox
  on JexlBuilder
- Added SandboxedJexl3.java test
@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Feb 10, 2021

@pwntester As we agreed in github/securitylab#249 (comment), I've updated the query to take into account setting a sandbox. The query now tracks if a sandbox is set with JexlBuilder.sandbox() or JexlBuilder.uberspect() methods. I think if we consider setting JextUberspect as a sandbox (github/securitylab#249 (comment)), then we rely on developers that they implement the sandbox correctly. Therefore, following this assumption, I didn't add a check for new JexlSandbox(false) since it doesn't really guarantee that the sandbox is correct either. I also added new tests with sandboxes. Please have a look.

- Updated SandboxedJexlFlowConfig to cover JEXL 2
- Added SandboxedJexl2 test
@artem-smotrakov
Copy link
Contributor Author

@artem-smotrakov artem-smotrakov commented Feb 11, 2021

I just realized that I forgot to include sandboxes for JEXL 2. Now it is covered. Added a test for that.

@pwntester
Copy link
Contributor

@pwntester pwntester commented Feb 12, 2021

Thanks, will request a new run from the CodeQL team and get back to you as soon as possible

override predicate isSource(DataFlow::Node node) { node instanceof SandboxedJexlSource }

override predicate isSink(DataFlow::Node node) {
node.asExpr().getType() instanceof JexlEngine or

This comment has been minimized.

@smowton

smowton Feb 15, 2021
Contributor

This could produce a pretty large set of sinks. Looks like we're only interested in qualifiers to CreateJexlScriptMethod calls and similar -- suggest restricting this to only consider the arguments we will consider in TaintPropagatingJexlMethodCall's characteristic predicate.


/**
* Holds if `fromNode` to `toNode` is a dataflow step that returns data from
* a tainted bean by calling one of its getters.

This comment has been minimized.

@smowton

smowton Feb 15, 2021
Contributor

Suggested change
* a tainted bean by calling one of its getters.
* a bean by calling one of its getters.
}

/**
* Method in the `JexlEngine` class that get or set a property with a Jexl expression.

This comment has been minimized.

@smowton

smowton Feb 15, 2021
Contributor

Suggested change
* Method in the `JexlEngine` class that get or set a property with a Jexl expression.
* A method in the `JexlEngine` class that get or set a property with a Jexl expression.
}

/**
* Defines methods that create a Jexl script.

This comment has been minimized.

@smowton

smowton Feb 15, 2021
Contributor

Suggested change
* Defines methods that create a Jexl script.
* A methods that create a Jexl script.

And similarly other uses of Defines ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

4 participants