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 slots = Collections.singletonMap( "values_a1", FormulaType.DECIMAL_LIST); Map 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 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 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 decimalItems(int count) { List 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()); } } }