Skip to content

Commit 7149880

Browse files
authored
feat: block AviatorScript-specific syntax in eval() for cross-platform compatibility (#514)
1 parent 97d474b commit 7149880

File tree

5 files changed

+388
-0
lines changed

5 files changed

+388
-0
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,43 @@ https://casbin.org/docs/adapters
187187

188188
https://casbin.org/docs/role-managers
189189

190+
## Expression Validation and Cross-Platform Compatibility
191+
192+
Starting from version 1.98.1, jCasbin validates expressions to ensure cross-platform compatibility with other Casbin implementations (Go, Node.js, Python, .NET, etc.).
193+
194+
### Restricted Syntax
195+
196+
The following AviatorScript-specific features are **not allowed** in `eval()` expressions and policy rules:
197+
198+
- **Namespace methods**: `seq.list()`, `string.startsWith()`, `string.endsWith()`, `math.sqrt()`, etc.
199+
- **Advanced control structures**: `lambda`, `let`, `fn`, `for`, `while`, `return`, `if-then-else`, `->`
200+
201+
These features are restricted because they are specific to AviatorScript and would make policies incompatible with other Casbin implementations.
202+
203+
### Allowed Syntax
204+
205+
The following standard Casbin syntax is fully supported:
206+
207+
- **Operators**: `&&`, `||`, `==`, `!=`, `<`, `>`, `<=`, `>=`, `+`, `-`, `*`, `/`, `!`, `in`
208+
- **Built-in functions**: `g()`, `keyMatch()`, `keyMatch2-5()`, `regexMatch()`, `ipMatch()`, `globMatch()`, `timeMatch()`, `eval()`
209+
- **Custom functions**: Users can still register custom functions using `enforcer.addFunction()`
210+
- **Variable access**: `r.attr`, `p.attr` (automatically escaped to `r_attr`, `p_attr`)
211+
212+
### Example
213+
214+
```java
215+
// ❌ NOT allowed - AviatorScript-specific syntax
216+
"eval(seq.list('admin', 'editor'))"
217+
"eval(string.startsWith(r.path, '/admin'))"
218+
219+
// ✅ Allowed - Standard Casbin syntax
220+
"eval(r.age > 18 && r.age < 65)"
221+
"r.role in ('admin', 'editor')" // Converted to include(tuple(...), ...)
222+
"g(r.sub, p.sub) && keyMatch(r.path, p.path)"
223+
```
224+
225+
If an expression contains restricted syntax, it will be logged as a warning and return `false`.
226+
190227
## Examples
191228

192229
| Model | Model file | Policy file |

src/main/java/org/casbin/jcasbin/util/BuiltInFunctions.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,15 @@ public String getName() {
488488
* @return the result of the eval.
489489
*/
490490
public static boolean eval(String eval, Map<String, Object> env, AviatorEvaluatorInstance aviatorEval) {
491+
// Validate expression to block AviatorScript-specific features
492+
// that break cross-platform compatibility
493+
try {
494+
ExpressionValidator.validateExpression(eval);
495+
} catch (IllegalArgumentException e) {
496+
Util.logPrintfWarn("Expression validation failed: {}", e.getMessage());
497+
return false;
498+
}
499+
491500
boolean res;
492501
if (aviatorEval != null) {
493502
try {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2024 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package org.casbin.jcasbin.util;
16+
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
19+
20+
/**
21+
* ExpressionValidator validates expressions to ensure they only use standard Casbin syntax
22+
* and don't expose AviatorScript-specific features that would break cross-platform compatibility.
23+
*/
24+
public class ExpressionValidator {
25+
26+
// Patterns for AviatorScript-specific syntax that should be blocked
27+
private static final Pattern[] DISALLOWED_PATTERNS = {
28+
Pattern.compile("\\bseq\\."), // seq.list(), seq.map(), etc.
29+
Pattern.compile("\\bstring\\."), // string.startsWith(), string.endsWith(), etc.
30+
Pattern.compile("\\bmath\\."), // math.sqrt(), math.pow(), etc.
31+
Pattern.compile("\\blambda\\b"), // lambda expressions
32+
Pattern.compile("\\blet\\b"), // variable binding
33+
Pattern.compile("\\bfn\\b"), // function definitions
34+
Pattern.compile("->"), // lambda arrow
35+
Pattern.compile("=>"), // alternative lambda arrow
36+
Pattern.compile("\\bfor\\b"), // for loops
37+
Pattern.compile("\\bwhile\\b"), // while loops
38+
Pattern.compile("\\breturn\\b"), // return statements
39+
Pattern.compile("\\bif\\b.*\\bthen\\b.*\\belse\\b"), // if-then-else (aviator style)
40+
};
41+
42+
/**
43+
* Validates that an expression only uses standard Casbin syntax.
44+
*
45+
* @param expression the expression to validate
46+
* @throws IllegalArgumentException if the expression contains non-standard syntax
47+
*/
48+
public static void validateExpression(String expression) {
49+
if (expression == null || expression.trim().isEmpty()) {
50+
return;
51+
}
52+
53+
// Check for disallowed AviatorScript-specific patterns
54+
for (Pattern pattern : DISALLOWED_PATTERNS) {
55+
Matcher matcher = pattern.matcher(expression);
56+
if (matcher.find()) {
57+
throw new IllegalArgumentException(
58+
"Expression contains non-standard syntax: '" + matcher.group() +
59+
"'. This AviatorScript-specific feature is not part of Casbin's standard specification."
60+
);
61+
}
62+
}
63+
}
64+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright 2024 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package org.casbin.jcasbin.main;
16+
17+
import org.casbin.jcasbin.util.ExpressionValidator;
18+
import org.testng.annotations.Test;
19+
20+
import static org.testng.Assert.*;
21+
22+
public class ExpressionValidatorTest {
23+
24+
@Test
25+
public void testValidStandardCasbinExpressions() {
26+
// Standard operators and comparisons should be allowed
27+
ExpressionValidator.validateExpression("r_sub == p_sub");
28+
ExpressionValidator.validateExpression("r_sub == p_sub && r_obj == p_obj");
29+
ExpressionValidator.validateExpression("r_sub == p_sub || r_obj == p_obj");
30+
ExpressionValidator.validateExpression("r_age > 18");
31+
ExpressionValidator.validateExpression("r_age >= 18 && r_age < 65");
32+
ExpressionValidator.validateExpression("r_status != 'banned'");
33+
34+
// Arithmetic should be allowed
35+
ExpressionValidator.validateExpression("r_price * 1.1 > p_threshold");
36+
ExpressionValidator.validateExpression("r_count + p_offset < 100");
37+
ExpressionValidator.validateExpression("r_value - p_discount >= 0");
38+
ExpressionValidator.validateExpression("r_total / r_count > 50");
39+
40+
// Negation should be allowed
41+
ExpressionValidator.validateExpression("!r_disabled");
42+
ExpressionValidator.validateExpression("!(r_sub == p_sub)");
43+
}
44+
45+
@Test
46+
public void testValidCasbinBuiltInFunctions() {
47+
// All standard Casbin functions should be allowed
48+
ExpressionValidator.validateExpression("g(r_sub, p_sub)");
49+
ExpressionValidator.validateExpression("g2(r_sub, p_sub, r_domain)");
50+
ExpressionValidator.validateExpression("keyMatch(r_path, p_path)");
51+
ExpressionValidator.validateExpression("keyMatch2(r_path, p_path)");
52+
ExpressionValidator.validateExpression("keyMatch3(r_path, p_path)");
53+
ExpressionValidator.validateExpression("keyMatch4(r_path, p_path)");
54+
ExpressionValidator.validateExpression("keyMatch5(r_path, p_path)");
55+
ExpressionValidator.validateExpression("keyGet(r_path, p_path)");
56+
ExpressionValidator.validateExpression("keyGet2(r_path, p_path, 'id')");
57+
ExpressionValidator.validateExpression("regexMatch(r_path, p_pattern)");
58+
ExpressionValidator.validateExpression("ipMatch(r_ip, p_cidr)");
59+
ExpressionValidator.validateExpression("globMatch(r_path, p_glob)");
60+
ExpressionValidator.validateExpression("allMatch(r_key, p_key)");
61+
ExpressionValidator.validateExpression("timeMatch(r_time, p_time)");
62+
ExpressionValidator.validateExpression("eval(p_rule)");
63+
64+
// Include and tuple are used for "in" operator conversion
65+
ExpressionValidator.validateExpression("include(r_obj, r_sub)");
66+
ExpressionValidator.validateExpression("include(tuple('admin', 'editor'), r_role)");
67+
68+
// Custom functions should be allowed (users can register them)
69+
ExpressionValidator.validateExpression("customFunc(r_sub, p_sub)");
70+
ExpressionValidator.validateExpression("myFunction(r_value)");
71+
}
72+
73+
@Test
74+
public void testValidComplexExpressions() {
75+
// Complex combinations should be allowed
76+
ExpressionValidator.validateExpression("g(r_sub, p_sub) && r_obj == p_obj && r_act == p_act");
77+
ExpressionValidator.validateExpression("g(r_sub, p_sub) && keyMatch(r_path, p_path)");
78+
ExpressionValidator.validateExpression("eval(p_sub_rule) && r_obj == p_obj");
79+
ExpressionValidator.validateExpression("r_age > 18 && include(tuple('read', 'write'), r_act)");
80+
ExpressionValidator.validateExpression("r_sub.age >= 18 && custom(r_obj)");
81+
}
82+
83+
@Test
84+
public void testDisallowedAviatorScriptSequenceMethods() {
85+
// seq.list() should be disallowed
86+
try {
87+
ExpressionValidator.validateExpression("seq.list('A', 'B')");
88+
fail("Should have thrown IllegalArgumentException for seq.list()");
89+
} catch (IllegalArgumentException e) {
90+
assertTrue(e.getMessage().contains("seq."));
91+
assertTrue(e.getMessage().contains("AviatorScript-specific"));
92+
}
93+
94+
// seq.map() should be disallowed
95+
try {
96+
ExpressionValidator.validateExpression("seq.map(r_items, lambda(x) -> x * 2)");
97+
fail("Should have thrown IllegalArgumentException for seq.map()");
98+
} catch (IllegalArgumentException e) {
99+
assertTrue(e.getMessage().contains("seq."));
100+
}
101+
}
102+
103+
@Test
104+
public void testDisallowedAviatorScriptStringMethods() {
105+
// string.startsWith() should be disallowed
106+
try {
107+
ExpressionValidator.validateExpression("string.startsWith(r_path, '/admin')");
108+
fail("Should have thrown IllegalArgumentException for string.startsWith()");
109+
} catch (IllegalArgumentException e) {
110+
assertTrue(e.getMessage().contains("string."));
111+
assertTrue(e.getMessage().contains("AviatorScript-specific"));
112+
}
113+
114+
// string.endsWith() should be disallowed
115+
try {
116+
ExpressionValidator.validateExpression("string.endsWith(r_path, '.pdf')");
117+
fail("Should have thrown IllegalArgumentException for string.endsWith()");
118+
} catch (IllegalArgumentException e) {
119+
assertTrue(e.getMessage().contains("string."));
120+
}
121+
122+
// string.substring() should be disallowed
123+
try {
124+
ExpressionValidator.validateExpression("string.substring(r_path, 0, 5)");
125+
fail("Should have thrown IllegalArgumentException for string.substring()");
126+
} catch (IllegalArgumentException e) {
127+
assertTrue(e.getMessage().contains("string."));
128+
}
129+
}
130+
131+
@Test
132+
public void testDisallowedAviatorScriptMathMethods() {
133+
// math.sqrt() should be disallowed
134+
try {
135+
ExpressionValidator.validateExpression("math.sqrt(r_value) > 10");
136+
fail("Should have thrown IllegalArgumentException for math.sqrt()");
137+
} catch (IllegalArgumentException e) {
138+
assertTrue(e.getMessage().contains("math."));
139+
assertTrue(e.getMessage().contains("AviatorScript-specific"));
140+
}
141+
142+
// math.pow() should be disallowed
143+
try {
144+
ExpressionValidator.validateExpression("math.pow(r_base, 2)");
145+
fail("Should have thrown IllegalArgumentException for math.pow()");
146+
} catch (IllegalArgumentException e) {
147+
assertTrue(e.getMessage().contains("math."));
148+
}
149+
}
150+
151+
@Test
152+
public void testDisallowedLambdaExpressions() {
153+
// Lambda with arrow should be disallowed
154+
try {
155+
ExpressionValidator.validateExpression("lambda(x) -> x * 2");
156+
fail("Should have thrown IllegalArgumentException for lambda");
157+
} catch (IllegalArgumentException e) {
158+
assertTrue(e.getMessage().contains("lambda") || e.getMessage().contains("->"));
159+
}
160+
161+
// Alternative lambda syntax should be disallowed
162+
try {
163+
ExpressionValidator.validateExpression("(x) => x * 2");
164+
fail("Should have thrown IllegalArgumentException for lambda arrow");
165+
} catch (IllegalArgumentException e) {
166+
assertTrue(e.getMessage().contains("=>"));
167+
}
168+
}
169+
170+
@Test
171+
public void testDisallowedControlStructures() {
172+
// for loops should be disallowed
173+
try {
174+
ExpressionValidator.validateExpression("for x in r_items { x * 2 }");
175+
fail("Should have thrown IllegalArgumentException for 'for'");
176+
} catch (IllegalArgumentException e) {
177+
assertTrue(e.getMessage().contains("for"));
178+
}
179+
180+
// while loops should be disallowed
181+
try {
182+
ExpressionValidator.validateExpression("while x < 10 { x = x + 1 }");
183+
fail("Should have thrown IllegalArgumentException for 'while'");
184+
} catch (IllegalArgumentException e) {
185+
assertTrue(e.getMessage().contains("while"));
186+
}
187+
188+
// if-then-else (Aviator style) should be disallowed
189+
try {
190+
ExpressionValidator.validateExpression("if r_age > 18 then 'adult' else 'minor'");
191+
fail("Should have thrown IllegalArgumentException for 'if-then-else'");
192+
} catch (IllegalArgumentException e) {
193+
assertTrue(e.getMessage().contains("if") || e.getMessage().contains("then") || e.getMessage().contains("else"));
194+
}
195+
}
196+
197+
@Test
198+
public void testDisallowedVariableBindingAndFunctions() {
199+
// let variable binding should be disallowed
200+
try {
201+
ExpressionValidator.validateExpression("let x = 10; x * 2");
202+
fail("Should have thrown IllegalArgumentException for 'let'");
203+
} catch (IllegalArgumentException e) {
204+
assertTrue(e.getMessage().contains("let"));
205+
}
206+
207+
// function definitions should be disallowed
208+
try {
209+
ExpressionValidator.validateExpression("fn add(a, b) { a + b }");
210+
fail("Should have thrown IllegalArgumentException for 'fn'");
211+
} catch (IllegalArgumentException e) {
212+
assertTrue(e.getMessage().contains("fn"));
213+
}
214+
215+
// return statements should be disallowed
216+
try {
217+
ExpressionValidator.validateExpression("return r_value * 2");
218+
fail("Should have thrown IllegalArgumentException for 'return'");
219+
} catch (IllegalArgumentException e) {
220+
assertTrue(e.getMessage().contains("return"));
221+
}
222+
}
223+
224+
@Test
225+
public void testNullAndEmptyExpressions() {
226+
// Null and empty expressions should be allowed (no validation needed)
227+
ExpressionValidator.validateExpression(null);
228+
ExpressionValidator.validateExpression("");
229+
ExpressionValidator.validateExpression(" ");
230+
}
231+
}

0 commit comments

Comments
 (0)