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
257 changes: 257 additions & 0 deletions docs/ExpressionEvaluator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Expression Evaluator - Cross-Platform Compatible eval() Function

## Overview

The `ExpressionEvaluator` class provides a sandboxed expression evaluator for Casbin's `eval()` function. It ensures that expressions only use standard Casbin operations, preventing the use of aviatorscript-specific features that would break cross-platform compatibility.

## Problem Statement

The previous implementation of `eval()` directly used aviatorscript, which allowed the use of aviatorscript-specific syntax such as:
- `seq.list('A', 'B')` - Collection operations
- `string.startsWith()`, `string.endsWith()` - String namespace methods
- `math.abs()` - Math namespace functions
- `lambda()`, `fn()` - Function definitions
- `let`, `for`, `while` - Control flow statements

These features are:
1. **Not portable** across different Casbin implementations (Go, Node.js, Python, etc.)
2. **Security risks** as they expose operations beyond Casbin's specification
3. **Non-standard** and not part of the official Casbin expression syntax

## Solution

The `ExpressionEvaluator` validates expressions before evaluation to ensure they only contain standard Casbin operations.

## Allowed Expression Syntax

The following operations are allowed in `eval()` expressions:

### 1. Property Access
Access object properties using dot notation:
```java
r.sub.name
r.sub.age
r.obj.owner
p.sub_rule
```

### 2. Comparison Operators
```java
r.sub.age > 18
r.sub.age >= 18
r.sub.age < 60
r.sub.age <= 60
r.sub.name == 'alice'
r.sub.name != 'bob'
```

### 3. Logical Operators
```java
r.sub.age > 18 && r.sub.age < 60
r.sub.name == 'alice' || r.sub.name == 'bob'
!r.sub.isBlocked
```

### 4. Arithmetic Operators
```java
r.sub.age + 10 > 28
r.sub.age * 2 < 100
r.sub.score - 10 >= 80
r.sub.total / 2 > 50
r.sub.value % 10 == 0
```

### 5. Literals
- **String literals**: Use single quotes `'alice'`, `'data1'`
- **Number literals**: `18`, `60`, `3.14`
- **Boolean literals**: `true`, `false`

### 6. Registered Function Calls
Custom functions registered with the enforcer can be called:
```java
custom(r.obj)
keyMatch(r.sub, '/api/*')
regexMatch(r.obj, '^/data\\d+$')
```

## Blocked Operations

The following aviatorscript-specific operations are **NOT allowed** and will cause validation to fail:

### Namespace Operations
```java
seq.list('A', 'B') // ❌ Blocked
string.startsWith(r.obj, '/') // ❌ Blocked
math.abs(r.sub.age) // ❌ Blocked
```

### Function Definitions
```java
fn add(a, b) { return a + b } // ❌ Blocked
lambda(x) -> x + 1 // ❌ Blocked
```

### Control Flow
```java
let x = 10 // ❌ Blocked
for (i = 0; i < 10; i++) // ❌ Blocked
while (true) // ❌ Blocked
```

### Object Instantiation and Imports
```java
new java.util.ArrayList() // ❌ Blocked
import java.util.List // ❌ Blocked
```

## Usage

### In Policy Rules

Define expressions in your policy files that will be evaluated:

**Model** (`abac_rule_model.conf`):
```ini
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub_rule, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act
```

**Policy** (`abac_rule_policy.csv`):
```csv
p, r.sub.Age > 18, /data1, read
p, r.sub.Age < 60, /data2, write
```

### In Java Code

```java
import org.casbin.jcasbin.main.Enforcer;

// Create enforcer
Enforcer enforcer = new Enforcer("model.conf", "policy.csv");

// Create request subject with attributes
class User {
private String name;
private int age;
// getters...
}

User alice = new User("alice", 25);

// Enforce - the eval() function will be called with the expression from policy
boolean allowed = enforcer.enforce(alice, "/data1", "read");
// Returns true because alice.age (25) > 18
```

## Implementation Details

### ExpressionEvaluator Class

The `ExpressionEvaluator` class provides:

1. **validateExpression(String expression)**: Validates that an expression only contains standard Casbin operations
2. **evaluateExpression(String expression, Map<String, Object> env, AviatorEvaluatorInstance aviatorEval)**: Validates and evaluates an expression
3. **configureRestrictedEvaluator(AviatorEvaluatorInstance aviatorEval)**: Configures an aviator evaluator with restricted options

### EvalFunc Integration

The `EvalFunc` class (the wrapper for the `eval()` function) has been updated to use `ExpressionEvaluator`:

```java
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
String eval = FunctionUtils.getStringValue(arg1, env);
eval = replaceTargets(eval, env);

// Validate expression to ensure it only uses standard Casbin operations
try {
ExpressionEvaluator.validateExpression(eval);
} catch (IllegalArgumentException e) {
Util.logPrintfWarn("Invalid eval expression: {}", e.getMessage());
return AviatorBoolean.valueOf(false);
}

return AviatorBoolean.valueOf(BuiltInFunctions.eval(eval, env, getAviatorEval()));
}
```

## Error Handling

When an invalid expression is detected:

1. An `IllegalArgumentException` is thrown with a descriptive error message
2. The error is logged as a warning
3. The evaluation returns `false` to fail safely

Example error message:
```
Expression contains non-standard Casbin operations.
Please use only standard operators and registered functions.
Expression: seq.list('A', 'B')
```

## Migration Guide

### For Existing Code

If your existing policies use standard Casbin syntax (property access, comparison operators, logical operators), **no changes are needed**. The validator allows all standard operations.

### If Using Aviatorscript-Specific Features

If you have policies that use aviatorscript-specific features, you need to migrate them:

**Before** (aviatorscript-specific):
```csv
p, include(seq.list('admin', 'moderator'), r.sub.role), /data1, read
p, string.startsWith(r.obj, '/api'), /api, GET
```

**After** (standard Casbin):
```csv
p, r.sub.role == 'admin' || r.sub.role == 'moderator', /data1, read
p, keyMatch(r.obj, '/api/*'), /api, GET
```

Or use registered custom functions for complex logic:
```java
// Register a custom function
enforcer.addFunction("checkRole", new CustomRoleFunction());
```

## Testing

Comprehensive tests are provided in `ExpressionEvaluatorTest.java`:

- Tests for valid standard Casbin expressions
- Tests for invalid aviatorscript-specific features
- Tests for null/empty expressions
- Integration tests with actual evaluation

Run tests:
```bash
mvn test -Dtest=ExpressionEvaluatorTest
```

## Benefits

1. **Cross-Platform Compatibility**: Expressions work the same way across all Casbin implementations
2. **Security**: Prevents execution of arbitrary code beyond Casbin specification
3. **Portability**: Policies can be easily migrated between different platforms
4. **Clarity**: Clear definition of what operations are allowed in expressions
5. **Backward Compatibility**: Existing standard expressions continue to work without changes

## References

- [Casbin Documentation](https://casbin.org/docs/)
- [ABAC Model Documentation](https://casbin.org/docs/abac)
- [Expression Syntax](https://casbin.org/docs/syntax-for-models)
107 changes: 107 additions & 0 deletions src/main/java/org/casbin/jcasbin/util/ExpressionEvaluator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2024 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.casbin.jcasbin.util;

import com.googlecode.aviator.AviatorEvaluatorInstance;
import com.googlecode.aviator.Options;

import java.util.Map;
import java.util.regex.Pattern;

/**
* ExpressionEvaluator provides a sandboxed expression evaluator for Casbin.
* It restricts expressions to only standard Casbin operations, preventing the use
* of aviatorscript-specific features that would break cross-platform compatibility.
*
* @author casbin team
*/
public class ExpressionEvaluator {

// Pattern to detect potentially unsafe aviatorscript-specific features
// Case-insensitive to prevent bypasses with different casing
private static final Pattern UNSAFE_PATTERN = Pattern.compile(
"(?i)(?:seq\\.|string\\.|math\\.|" + // aviatorscript namespace calls (case-insensitive)
"include\\(\\s*seq\\.list|" + // aviatorscript collection operations
"lambda\\(|" + // lambda expressions
"let\\s+|" + // let bindings
"\\bfn\\s+|" + // function definitions
"\\bfor\\s*\\(|" + // for loops
"\\bwhile\\s*\\(|" + // while loops
"\\bnew\\s+|" + // object instantiation
"\\bimport\\s+)" // imports
);

/**
* Validates that an expression only contains standard Casbin operations.
*
* @param expression the expression to validate
* @throws IllegalArgumentException if the expression contains unsafe operations
*/
public static void validateExpression(String expression) {
if (expression == null || expression.isEmpty()) {
throw new IllegalArgumentException("Expression cannot be null or empty");
}

if (UNSAFE_PATTERN.matcher(expression).find()) {
throw new IllegalArgumentException(
"Expression contains non-standard Casbin operations. " +
"Please use only standard operators and registered functions. " +
"Expression: " + expression
);
}
}

/**
* Configures an AviatorEvaluatorInstance with restricted options for safe evaluation.
* This disables features that go beyond standard Casbin expression evaluation.
*
* @param aviatorEval the evaluator instance to configure
* @return the configured evaluator instance
*/
public static AviatorEvaluatorInstance configureRestrictedEvaluator(AviatorEvaluatorInstance aviatorEval) {
if (aviatorEval == null) {
return null;
}

// Create an empty feature set to disable all extra features for security
// This restricts the evaluator to only basic expression evaluation
java.util.Set<com.googlecode.aviator.Feature> restrictedFeatures = java.util.Collections.emptySet();
aviatorEval.setOption(Options.FEATURE_SET, restrictedFeatures);

// Use EVAL optimization level: This provides compile-once execution for expressions.
// EVAL is the most basic optimization level that compiles expressions without
// aggressive optimizations, providing a balance between safety and performance.
aviatorEval.setOption(Options.OPTIMIZE_LEVEL, com.googlecode.aviator.AviatorEvaluator.EVAL);

return aviatorEval;
}

/**
* Safely evaluates an expression with validation.
*
* @param expression the expression to evaluate
* @param env the evaluation environment containing variables
* @param aviatorEval the aviator evaluator instance (can be null)
* @return the evaluation result
* @throws IllegalArgumentException if the expression is invalid
*/
public static boolean evaluateExpression(String expression, Map<String, Object> env, AviatorEvaluatorInstance aviatorEval) {
// Validate expression first
validateExpression(expression);

// Evaluate using the existing BuiltInFunctions.eval method
return BuiltInFunctions.eval(expression, env, aviatorEval);
}
}
16 changes: 16 additions & 0 deletions src/main/java/org/casbin/jcasbin/util/function/EvalFunc.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.googlecode.aviator.runtime.type.AviatorBoolean;
import com.googlecode.aviator.runtime.type.AviatorObject;
import org.casbin.jcasbin.util.BuiltInFunctions;
import org.casbin.jcasbin.util.ExpressionEvaluator;
import org.casbin.jcasbin.util.Util;

import java.util.Map;

Expand All @@ -33,6 +35,20 @@ public class EvalFunc extends CustomFunction {
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
String eval = FunctionUtils.getStringValue(arg1, env);
eval = replaceTargets(eval, env);

// Validate expression to ensure it only uses standard Casbin operations
// This is a security-critical validation to prevent non-standard operations
try {
ExpressionEvaluator.validateExpression(eval);
} catch (IllegalArgumentException e) {
// Log at ERROR level for security violations to ensure visibility
// Do not log the full expression to avoid exposing potentially malicious content
Util.logPrintfError("Security violation - invalid eval expression rejected. " +
"Expression contains non-standard Casbin operations that are not allowed.");
// Return false to fail safely rather than throwing, which could break policy evaluation
return AviatorBoolean.valueOf(false);
}

return AviatorBoolean.valueOf(BuiltInFunctions.eval(eval, env, getAviatorEval()));
}

Expand Down
Loading
Loading