#pragma once // Step 570: Watch Expression Evaluator Hardening #include #include #include #include #include struct WatchEvalLimits { std::size_t maxExpressionLength = 256; std::size_t maxTokens = 64; std::size_t maxOperations = 32; std::uint64_t timeoutBudgetMs = 32; }; struct WatchEvalResult { bool ok = false; bool timedOut = false; bool rejectedUnsafe = false; std::int64_t value = 0; std::size_t operations = 0; std::string error; }; class WatchExpressionEvaluatorHardening { public: static WatchEvalResult evaluate(const std::string& expression, const std::map& scope, const WatchEvalLimits& limits = {}) { WatchEvalResult result; if (expression.empty()) { fail(result, "expression_empty"); return result; } if (expression.size() > limits.maxExpressionLength) { fail(result, "expression_too_long"); return result; } if (containsUnsafePattern(expression)) { result.rejectedUnsafe = true; fail(result, "expression_unsafe"); return result; } std::vector tokens; if (!tokenize(expression, limits.maxTokens, &tokens, &result.error)) return result; if (tokens.empty()) { fail(result, "expression_empty"); return result; } std::vector values; std::vector operators; for (const auto& token : tokens) { if (isOperatorToken(token)) { operators.push_back(token[0]); } else { std::int64_t value = 0; if (!parseValue(token, scope, &value, &result.error)) return result; values.push_back(value); } } if (values.empty()) { fail(result, "value_missing"); return result; } if (values.size() != operators.size() + 1) { fail(result, "operator_operand_mismatch"); return result; } if (!applyOperations(values, operators, &result, limits)) return result; result.ok = true; return result; } private: static bool fail(WatchEvalResult& result, const char* code) { result.error = code; result.ok = false; return false; } static bool containsUnsafePattern(const std::string& expression) { static const char* badSubstrings[] = { ";", "{", "}", "[", "]", "__", "system", "exec", "include", "while", "for(" }; for (const auto* pattern : badSubstrings) { if (expression.find(pattern) != std::string::npos) return true; } return false; } static bool tokenize(const std::string& expression, std::size_t maxTokens, std::vector* outTokens, std::string* error) { std::string current; auto flushCurrent = [&]() { if (!current.empty()) { outTokens->push_back(current); current.clear(); } }; for (char c : expression) { if (std::isspace(static_cast(c))) { flushCurrent(); continue; } if (c == '+' || c == '-' || c == '*' || c == '/') { flushCurrent(); outTokens->push_back(std::string(1, c)); } else if (std::isalnum(static_cast(c)) || c == '_') { current.push_back(c); } else { *error = "token_invalid_character"; return false; } if (outTokens->size() > maxTokens) { *error = "token_limit_exceeded"; return false; } } flushCurrent(); if (outTokens->size() > maxTokens) { *error = "token_limit_exceeded"; return false; } return true; } static bool isOperatorToken(const std::string& token) { return token.size() == 1 && (token[0] == '+' || token[0] == '-' || token[0] == '*' || token[0] == '/'); } static bool parseValue(const std::string& token, const std::map& scope, std::int64_t* outValue, std::string* error) { if (token.empty()) { *error = "value_missing"; return false; } bool numeric = true; for (char c : token) { if (!std::isdigit(static_cast(c))) { numeric = false; break; } } if (numeric) { *outValue = std::stoll(token); return true; } auto it = scope.find(token); if (it == scope.end()) { *error = "identifier_unknown"; return false; } *outValue = it->second; return true; } static bool applyOperations(std::vector values, std::vector operators, WatchEvalResult* result, const WatchEvalLimits& limits) { std::vector reducedValues; std::vector reducedOperators; reducedValues.push_back(values[0]); for (std::size_t i = 0; i < operators.size(); ++i) { const char op = operators[i]; const std::int64_t rhs = values[i + 1]; if (op == '*' || op == '/') { const std::int64_t lhs = reducedValues.back(); if (op == '/' && rhs == 0) return fail(*result, "division_by_zero"); reducedValues.back() = op == '*' ? lhs * rhs : lhs / rhs; if (!countOperation(result, limits)) return false; } else { reducedValues.push_back(rhs); reducedOperators.push_back(op); } } std::int64_t acc = reducedValues[0]; for (std::size_t i = 0; i < reducedOperators.size(); ++i) { const char op = reducedOperators[i]; const std::int64_t rhs = reducedValues[i + 1]; acc = op == '+' ? acc + rhs : acc - rhs; if (!countOperation(result, limits)) return false; } result->value = acc; return true; } static bool countOperation(WatchEvalResult* result, const WatchEvalLimits& limits) { ++result->operations; if (result->operations > limits.maxOperations) return fail(*result, "operation_limit_exceeded"); if (result->operations > limits.timeoutBudgetMs) { result->timedOut = true; return fail(*result, "evaluation_timeout"); } return true; } };