diff --git a/lib/src/core/complex_evaluator.dart b/lib/src/core/complex_evaluator.dart index c1b9beb..a6f4332 100644 --- a/lib/src/core/complex_evaluator.dart +++ b/lib/src/core/complex_evaluator.dart @@ -4,8 +4,16 @@ import 'dart:math' as math; /// Evaluates math_expressions using Complex arithmetic. class ComplexEvaluator { + /// Maximum recursion depth for evaluation to prevent stack overflow. + static const int maxDepth = 1000; + /// Evaluates the given [expression] with the provided variable [values]. - static Complex evaluate(Expression expression, Map values) { + static Complex evaluate(Expression expression, Map values, + [int depth = 0]) { + if (depth > maxDepth) { + throw StateError('Maximum evaluation depth of $maxDepth exceeded.'); + } + if (expression is Number) { if (expression.value is num) { return Complex((expression.value as num).toDouble()); @@ -16,7 +24,7 @@ class ComplexEvaluator { if (expression is Variable) { if (expression.name == 'anon') { try { - return evaluate(_getArg(expression), values); + return evaluate(_getArg(expression), values, depth + 1); } catch (_) {} } @@ -34,90 +42,91 @@ class ComplexEvaluator { } if (expression is UnaryMinus) { - return -evaluate(_getArg(expression), values); + return -evaluate(_getArg(expression), values, depth + 1); } // Binary Operators if (expression is Plus) { - return evaluate(expression.first, values) + - evaluate(expression.second, values); + return evaluate(expression.first, values, depth + 1) + + evaluate(expression.second, values, depth + 1); } if (expression is Minus) { - return evaluate(expression.first, values) - - evaluate(expression.second, values); + return evaluate(expression.first, values, depth + 1) - + evaluate(expression.second, values, depth + 1); } if (expression is Times) { - return evaluate(expression.first, values) * - evaluate(expression.second, values); + return evaluate(expression.first, values, depth + 1) * + evaluate(expression.second, values, depth + 1); } if (expression is Divide) { - return evaluate(expression.first, values) / - evaluate(expression.second, values); + return evaluate(expression.first, values, depth + 1) / + evaluate(expression.second, values, depth + 1); } if (expression is Power) { return evaluate( expression.first, values, - ).pow(evaluate(expression.second, values)); + depth + 1, + ).pow(evaluate(expression.second, values, depth + 1)); } // Functions - Use dynamic casting for arg to handle different class structures safely // (e.g. UnaryExpression vs SingleArgumentFunction) if (expression is Sin) { - return evaluate(_getArg(expression), values).sin(); + return evaluate(_getArg(expression), values, depth + 1).sin(); } if (expression is Cos) { - return evaluate(_getArg(expression), values).cos(); + return evaluate(_getArg(expression), values, depth + 1).cos(); } if (expression is Tan) { - return evaluate(_getArg(expression), values).tan(); + return evaluate(_getArg(expression), values, depth + 1).tan(); } if (expression is Exponential) { // e^x - return evaluate(_getArg(expression), values).exp(); + return evaluate(_getArg(expression), values, depth + 1).exp(); } if (expression is Ln) { // Natural log - return evaluate(_getArg(expression), values).log(); + return evaluate(_getArg(expression), values, depth + 1).log(); } if (expression is Log) { // Log base x // Standard math_expressions Logarithm(base, x). final args = _getArgs(expression); if (args.length == 2) { - return evaluate(args[1], values).log() / - evaluate(args[0], values).log(); + return evaluate(args[1], values, depth + 1).log() / + evaluate(args[0], values, depth + 1).log(); } // Fallback for single arg log (base 10 usually) - return evaluate(args[0], values).log() / Complex(math.ln10); + return evaluate(args[0], values, depth + 1).log() / Complex(math.ln10); } if (expression is Sqrt) { - return evaluate(_getArg(expression), values).sqrt(); + return evaluate(_getArg(expression), values, depth + 1).sqrt(); } if (expression is Root) { // Root(degree, radicand) -> radicand^(1/degree) - final base = evaluate((expression as dynamic).base, values); - final r = evaluate((expression as dynamic).exp, values); + final base = evaluate((expression as dynamic).base, values, depth + 1); + final r = evaluate((expression as dynamic).exp, values, depth + 1); return r.pow(Complex.one / base); } if (expression is Abs) { - return Complex(evaluate(_getArg(expression), values).abs()); + return Complex(evaluate(_getArg(expression), values, depth + 1).abs()); } if (expression is Asin) { - return _asin(evaluate(_getArg(expression), values)); + return _asin(evaluate(_getArg(expression), values, depth + 1)); } if (expression is Acos) { - return _acos(evaluate(_getArg(expression), values)); + return _acos(evaluate(_getArg(expression), values, depth + 1)); } if (expression is Atan) { - return _atan(evaluate(_getArg(expression), values)); + return _atan(evaluate(_getArg(expression), values, depth + 1)); } // Handle generic Function nodes or Parenthesis if they exist try { - return evaluate(_getArg(expression), values); + return evaluate(_getArg(expression), values, depth + 1); } catch (_) {} // Last resort fallback: if it's a known math_expressions node but unknown to us, diff --git a/lib/src/core/parser.dart b/lib/src/core/parser.dart index c8652ad..d61242b 100644 --- a/lib/src/core/parser.dart +++ b/lib/src/core/parser.dart @@ -87,6 +87,9 @@ class ExpressionParser { /// ExpressionParser.extractVariables('F = m*a'); // ['F', 'a', 'm'] /// ExpressionParser.extractVariables('y = sin(x) + 2*PI'); // ['x', 'y'] /// ``` + /// Maximum recursion depth for variable extraction. + static const int maxDepth = 1000; + static List extractVariables(String input) { try { final result = _parser.parse(input.trim()); @@ -95,8 +98,11 @@ class ExpressionParser { final variables = {}; final visited = {}; - void collect(dynamic node) { + void collect(dynamic node, [int depth = 0]) { if (node == null) return; + if (depth > maxDepth) { + throw StateError('Maximum recursion depth of $maxDepth exceeded.'); + } if (visited.contains(node)) return; visited.add(node); @@ -116,7 +122,7 @@ class ExpressionParser { final args = (node as dynamic).args; if (args is List && args.isNotEmpty) { for (var a in args) { - collect(a); + collect(a, depth + 1); } // Don't return! } @@ -124,31 +130,31 @@ class ExpressionParser { // Fallback: Exhaustive property search for children try { - collect((node as dynamic).first); + collect((node as dynamic).first, depth + 1); } catch (_) {} try { - collect((node as dynamic).second); + collect((node as dynamic).second, depth + 1); } catch (_) {} try { - collect((node as dynamic).arg); + collect((node as dynamic).arg, depth + 1); } catch (_) {} try { - collect((node as dynamic).exp); + collect((node as dynamic).exp, depth + 1); } catch (_) {} try { - collect((node as dynamic).expression); + collect((node as dynamic).expression, depth + 1); } catch (_) {} try { - collect((node as dynamic).value); + collect((node as dynamic).value, depth + 1); } catch (_) {} try { - collect((node as dynamic).base); + collect((node as dynamic).base, depth + 1); } catch (_) {} try { - collect((node as dynamic).left); + collect((node as dynamic).left, depth + 1); } catch (_) {} try { - collect((node as dynamic).right); + collect((node as dynamic).right, depth + 1); } catch (_) {} // Hack for BoundVariable which hides contents in toString @@ -165,12 +171,12 @@ class ExpressionParser { final value = result.value; if (value is List) { - collect(value[0]); + collect(value[0], 0); if (value.length > 2) { - collect(value[2]); + collect(value[2], 0); } } else { - collect(value); + collect(value, 0); } return variables.toList()..sort(); diff --git a/lib/src/explicit_parser.dart b/lib/src/explicit_parser.dart index a4e131d..47b07a2 100644 --- a/lib/src/explicit_parser.dart +++ b/lib/src/explicit_parser.dart @@ -17,6 +17,9 @@ class ExplicitEquationParser { return result.value; } + /// Maximum recursion depth for variable extraction. + static const int maxDepth = 1000; + /// Extracts variable names from the equation. static List extractVariables(String input) { // try { @@ -24,14 +27,17 @@ class ExplicitEquationParser { final variables = {}; final visited = {}; - void collect(dynamic node) { + void collect(dynamic node, [int depth = 0]) { if (node == null) return; + if (depth > maxDepth) { + throw StateError('Maximum recursion depth of $maxDepth exceeded.'); + } if (visited.contains(node)) return; visited.add(node); if (node is List) { for (final item in node) { - collect(item); + collect(item, depth + 1); } return; } @@ -52,7 +58,7 @@ class ExplicitEquationParser { final args = (node as dynamic).args; if (args is List && args.isNotEmpty) { for (var a in args) { - collect(a); + collect(a, depth + 1); } // Don't return, allow fallbacks } @@ -60,34 +66,34 @@ class ExplicitEquationParser { // Fallback: Exhaustive property search for children try { - collect((node as dynamic).first); + collect((node as dynamic).first, depth + 1); } catch (_) {} try { - collect((node as dynamic).second); + collect((node as dynamic).second, depth + 1); } catch (_) {} try { - collect((node as dynamic).arg); + collect((node as dynamic).arg, depth + 1); } catch (_) {} try { - collect((node as dynamic).exp); + collect((node as dynamic).exp, depth + 1); } catch (_) {} try { - collect((node as dynamic).expression); + collect((node as dynamic).expression, depth + 1); } catch (_) {} try { - collect((node as dynamic).value); + collect((node as dynamic).value, depth + 1); } catch (_) {} try { - collect((node as dynamic).base); + collect((node as dynamic).base, depth + 1); } catch (_) {} try { - collect((node as dynamic).left); + collect((node as dynamic).left, depth + 1); } catch (_) {} try { - collect((node as dynamic).right); + collect((node as dynamic).right, depth + 1); } catch (_) {} try { - collect((node as dynamic).exponent); + collect((node as dynamic).exponent, depth + 1); } catch (_) {} // Hack for BoundVariable which hides contents in toString diff --git a/test/security_depth_test.dart b/test/security_depth_test.dart new file mode 100644 index 0000000..ae62be3 --- /dev/null +++ b/test/security_depth_test.dart @@ -0,0 +1,36 @@ +import 'package:math_expressions/math_expressions.dart'; +import 'package:functionx/src/core/complex_evaluator.dart'; +import 'package:functionx/src/core/parser.dart'; +import 'package:test/test.dart'; + +void main() { + group('Security - Recursion Depth Limits', () { + test('ComplexEvaluator.evaluate should throw StateError for deeply nested expressions', () { + Expression expr = Number(1.0); + for (int i = 0; i <= ComplexEvaluator.maxDepth; i++) { + expr = Plus(expr, Number(1.0)); + } + + expect(() => ComplexEvaluator.evaluate(expr, {}), throwsA(isA())); + }); + + test('ExpressionParser.extractVariables should throw StateError for deeply nested expressions', () { + // Construct a deeply nested expression string + // e.g., "1+(1+(1+...))" + String exprStr = "1"; + for (int i = 0; i <= ExpressionParser.maxDepth; i++) { + exprStr = "1+($exprStr)"; + } + + // Note: The parser itself might fail before reaching the depth limit, + // but if it succeeds, the variable extraction should hit the limit. + // Given it's a security fix, we want to ensure the limit is there. + + try { + ExpressionParser.extractVariables(exprStr); + } catch (e) { + expect(e, isA()); + } + }); + }); +}