package jnpf.elnFormula;
|
|
import org.junit.Test;
|
|
import java.math.RoundingMode;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.fail;
|
|
public class FormulaSecurityTest {
|
|
private final FormulaParser parser = new FormulaParser();
|
private final FormulaFunctionRegistry functions = new FormulaFunctionRegistry();
|
private final FormulaTypeChecker checker = new FormulaTypeChecker(functions);
|
private final FormulaEvaluator evaluator = new FormulaEvaluator(functions);
|
|
@Test
|
public void rejectsReflectionBeanFileNetworkSqlAndArbitraryMethodEntrypoints() {
|
assertRejected("java.lang.Runtime.getRuntime()", "LEXICAL_ERROR");
|
assertRejected("T(java.lang.System).exit(0)", "LEXICAL_ERROR");
|
assertRejected("bean.method()", "LEXICAL_ERROR");
|
assertRejected("READ_FILE(\"/etc/passwd\")", "UNKNOWN_FUNCTION");
|
assertRejected("HTTP_GET(\"https://example.com\")", "UNKNOWN_FUNCTION");
|
assertRejected("SQL(\"select 1\")", "UNKNOWN_FUNCTION");
|
assertRejected("method()", "SYNTAX_ERROR");
|
}
|
|
@Test
|
public void followsMathematicalPowerPrecedenceAndIntegerExponentRules() {
|
assertDecimal("2 ^ 3 ^ 2", "512");
|
assertDecimal("-2 ^ 2", "-4");
|
assertDecimal("2 ^ -2", "0.25");
|
assertFormulaError("2 ^ 1.5", "INVALID_EXPONENT");
|
assertFormulaError("2 ^ 1000", "INVALID_EXPONENT");
|
assertFormulaError("0 ^ -1", "DIVIDE_BY_ZERO");
|
assertDecimal("100000000000000000000000000000000000 % 3", "1");
|
}
|
|
@Test
|
public void shortCircuitsLogicalAndConditionalFunctions() {
|
assertBoolean("false && (1 / 0 > 0)", false);
|
assertBoolean("true || (1 / 0 > 0)", true);
|
assertDecimal("IF(true, 1, 1 / 0)", "1");
|
assertDecimal("COALESCE(null, 2, 1 / 0)", "2");
|
assertBoolean("IS_BLANK(\" \")", true);
|
assertBoolean("IS_BLANK(\"\u3000\")", true);
|
assertBoolean("IS_BLANK(0)", false);
|
assertBoolean("IS_BLANK(false)", false);
|
}
|
|
@Test
|
public void enforcesStrictTypesAndNumericBoundaries() {
|
assertFormulaError("1 == \"1\"", "TYPE_MISMATCH");
|
assertFormulaError("null == null", "TYPE_MISMATCH");
|
assertFormulaError("SQRT(-1)", "NEGATIVE_SQUARE_ROOT");
|
assertFormulaError("CONVERT(1, \"mg\", \"mL\")", "UNIT_DIMENSION_MISMATCH");
|
assertFormulaError("LOOKUP_LINEAR(30, 0, 0, 10, 1)", "OUT_OF_RANGE");
|
|
Map<String, FormulaType> slots = Collections.singletonMap(
|
"values_a1", FormulaType.DECIMAL_LIST);
|
Map<String, FormulaValue> values = Collections.singletonMap("values_a1",
|
FormulaValue.decimalList(FormulaValue.decimal("-1"), FormulaValue.decimal("1")));
|
FormulaAst.Node ast = parser.parse("RSD(values_a1)");
|
checker.check(ast, slots);
|
try {
|
evaluator.evaluate(ast, values);
|
fail("expected ZERO_MEAN");
|
} catch (FormulaException ex) {
|
assertEquals("ZERO_MEAN", ex.getCode());
|
}
|
}
|
|
@Test
|
public void roundsOnlyDecimalValuesWithRequestedMode() {
|
FormulaValue value = FormulaValue.decimal("2.345");
|
assertEquals("2.35", evaluator.round(value, 2, RoundingMode.HALF_UP).toPlainString());
|
assertEquals("2.34", evaluator.round(value, 2, RoundingMode.HALF_EVEN).toPlainString());
|
assertEquals("2.35", evaluator.round(value, 2, RoundingMode.UP).toPlainString());
|
assertEquals("2.34", evaluator.round(value, 2, RoundingMode.DOWN).toPlainString());
|
try {
|
evaluator.round(value, 2, RoundingMode.CEILING);
|
fail("expected UNSUPPORTED_ROUNDING_MODE");
|
} catch (FormulaException ex) {
|
assertEquals("UNSUPPORTED_ROUNDING_MODE", ex.getCode());
|
}
|
}
|
|
@Test
|
public void enforcesConfiguredDecimalListItemLimit() {
|
FormulaLimits customLimits = new FormulaLimits(8192, 4096, 64, 10000, 1, 256);
|
FormulaEvaluator constrainedEvaluator = new FormulaEvaluator(functions, customLimits);
|
FormulaAst.Node ast = parser.parse("SUM(values_a1)");
|
Map<String, FormulaValue> values = Collections.singletonMap("values_a1",
|
FormulaValue.decimalList(FormulaValue.decimal("1"), FormulaValue.decimal("1")));
|
|
try {
|
constrainedEvaluator.evaluate(ast, values);
|
fail("expected LIST_ITEM_LIMIT_EXCEEDED");
|
} catch (FormulaException ex) {
|
assertEquals("LIST_ITEM_LIMIT_EXCEEDED", ex.getCode());
|
}
|
}
|
|
@Test
|
public void enforcesDefaultDecimalListItemLimitAtEvaluatorBoundary() {
|
FormulaAst.Node ast = parser.parse("SUM(values_a1)");
|
Map<String, FormulaValue> maximum = Collections.singletonMap("values_a1",
|
FormulaValue.decimalList(decimalItems(1000)));
|
assertEquals("1000", evaluator.evaluate(ast, maximum).toPlainString());
|
|
FormulaValue overLimit = FormulaValue.decimalList(decimalItems(1001));
|
try {
|
evaluator.evaluate(ast, Collections.singletonMap("values_a1", overLimit));
|
fail("expected LIST_ITEM_LIMIT_EXCEEDED");
|
} catch (FormulaException ex) {
|
assertEquals("LIST_ITEM_LIMIT_EXCEEDED", ex.getCode());
|
}
|
}
|
|
private static List<FormulaValue> decimalItems(int count) {
|
List<FormulaValue> values = new ArrayList<>(count);
|
for (int i = 0; i < count; i++) {
|
values.add(FormulaValue.decimal("1"));
|
}
|
return values;
|
}
|
|
private void assertRejected(String expression, String code) {
|
try {
|
FormulaAst.Node ast = parser.parse(expression);
|
checker.check(ast, Collections.emptyMap());
|
fail("expected " + code);
|
} catch (FormulaParser.ParseException ex) {
|
assertEquals(code, ex.getCode());
|
} catch (FormulaException ex) {
|
assertEquals(code, ex.getCode());
|
}
|
}
|
|
private void assertDecimal(String expression, String expected) {
|
FormulaAst.Node ast = parser.parse(expression);
|
assertEquals(FormulaType.DECIMAL, checker.check(ast, Collections.emptyMap()));
|
assertEquals(expected, evaluator.evaluate(ast, Collections.emptyMap()).toPlainString());
|
}
|
|
private void assertBoolean(String expression, boolean expected) {
|
FormulaAst.Node ast = parser.parse(expression);
|
assertEquals(FormulaType.BOOLEAN, checker.check(ast, Collections.emptyMap()));
|
assertEquals(Boolean.toString(expected),
|
evaluator.evaluate(ast, Collections.emptyMap()).toPlainString());
|
}
|
|
private void assertFormulaError(String expression, String code) {
|
try {
|
FormulaAst.Node ast = parser.parse(expression);
|
checker.check(ast, Collections.emptyMap());
|
evaluator.evaluate(ast, new LinkedHashMap<>());
|
fail("expected " + code);
|
} catch (FormulaException ex) {
|
assertEquals(code, ex.getCode());
|
}
|
}
|
}
|