diff --git a/lib/base/json.cpp b/lib/base/json.cpp index d8a7b7a95..122d25b4e 100644 --- a/lib/base/json.cpp +++ b/lib/base/json.cpp @@ -343,12 +343,22 @@ public: Value GetResult(); private: + struct Node + { + Dictionary::Ptr m_Dictionary{}; + Array::Ptr m_Array{}; + String m_CurrentKey{}; + + explicit Node(Dictionary::Ptr dict) : m_Dictionary(std::move(dict)) {} + explicit Node(Array::Ptr array) : m_Array(std::move(array)) {} + }; + Value m_Root; - std::stack> m_CurrentSubtree; - String m_CurrentKey; + std::vector m_Stack; std::size_t m_DepthLimit; void FillCurrentTarget(Value value); + void CheckDepthLimit() const; }; String icinga::JsonEncode(const Value& value, bool prettify) @@ -455,14 +465,9 @@ inline bool JsonSax::start_object(std::size_t) { auto object (new Dictionary()); - FillCurrentTarget(object); - - if (m_CurrentSubtree.size() >= m_DepthLimit) { - BOOST_THROW_EXCEPTION(std::runtime_error("JSON decoding recursion limit reached")); - } - - m_CurrentSubtree.push({object, nullptr}); + CheckDepthLimit(); + m_Stack.push_back(Node{object}); return true; } @@ -470,7 +475,7 @@ bool JsonSax::start_object(std::size_t) inline bool JsonSax::key(JsonSax::string_t& val) { - m_CurrentKey = String(std::move(val)); + m_Stack.back().m_CurrentKey = String(std::move(val)); return true; } @@ -478,8 +483,7 @@ bool JsonSax::key(JsonSax::string_t& val) inline bool JsonSax::end_object() { - m_CurrentSubtree.pop(); - m_CurrentKey = String(); + m_Stack.pop_back(); return true; } @@ -488,14 +492,9 @@ inline bool JsonSax::start_array(std::size_t) { auto array (new Array()); - FillCurrentTarget(array); - - if (m_CurrentSubtree.size() >= m_DepthLimit) { - BOOST_THROW_EXCEPTION(std::runtime_error("JSON decoding recursion limit reached")); - } - - m_CurrentSubtree.push({nullptr, array}); + CheckDepthLimit(); + m_Stack.push_back(Node{array}); return true; } @@ -503,7 +502,7 @@ bool JsonSax::start_array(std::size_t) inline bool JsonSax::end_array() { - m_CurrentSubtree.pop(); + m_Stack.pop_back(); return true; } @@ -523,15 +522,34 @@ Value JsonSax::GetResult() inline void JsonSax::FillCurrentTarget(Value value) { - if (m_CurrentSubtree.empty()) { + if (m_Stack.empty()) { m_Root = value; } else { - auto& node (m_CurrentSubtree.top()); + auto& node (m_Stack.back()); - if (node.first) { - node.first->Set(m_CurrentKey, value); + if (node.m_Dictionary) { + node.m_Dictionary->Set(node.m_CurrentKey, value); } else { - node.second->Add(value); + node.m_Array->Add(value); } } } + +void JsonSax::CheckDepthLimit() const +{ + if (m_Stack.size() >= m_DepthLimit) { + std::ostringstream buf; + buf << "JSON decoding recursion limit reached (path: root"; + for (const auto& node : m_Stack) { + buf << "["; + if (node.m_Dictionary) { + buf << JsonEncode(node.m_CurrentKey); + } else { + buf << node.m_Array->GetLength() - 1; + } + buf << "]"; + } + buf << ")"; + BOOST_THROW_EXCEPTION(std::runtime_error(buf.str())); + } +} diff --git a/test/base-json.cpp b/test/base-json.cpp index c6b37affd..d5f46d64f 100644 --- a/test/base-json.cpp +++ b/test/base-json.cpp @@ -171,8 +171,23 @@ static std::string MakeNestedJsonObject(size_t depth) BOOST_AUTO_TEST_CASE(decode_depth_limit) { - auto isDepthLimit = [](const std::exception& ex) { - return std::string_view(ex.what()) == "JSON decoding recursion limit reached"; + auto ExpectLimitExceeded = [](std::string_view input, size_t limit, std::string_view path) { + try { + JsonDecode(std::string(input), limit); + + boost::test_tools::assertion_result result{false}; + result.message() << "Decoding '" << input << "' with limit " << limit << " did not throw an exception"; + return result; + } catch (const std::exception& ex) { + std::ostringstream expected; + expected << "JSON decoding recursion limit reached (path: " << path << ")"; + + boost::test_tools::assertion_result result{ex.what() == expected.str()}; + result.message() << "Decoding '" << input << "' with limit " << limit << ":\n" + << " got exception: " << ex.what() << "\n" + << " expected exception: " << expected.str() << "\n"; + return result; + } }; // Scalars parse even with depth limit 0. @@ -181,21 +196,21 @@ BOOST_AUTO_TEST_CASE(decode_depth_limit) BOOST_CHECK_EQUAL(JsonDecode("\"test\"", 0), Value("test")); // Arrays and objects require at least depth limit 1. - BOOST_CHECK_EXCEPTION(JsonDecode("[]", 0), std::exception, isDepthLimit); - BOOST_CHECK_EXCEPTION(JsonDecode("{}", 0), std::exception, isDepthLimit); - BOOST_CHECK_EXCEPTION(JsonDecode("[42]", 0), std::exception, isDepthLimit); - BOOST_CHECK_EXCEPTION(JsonDecode("{\"foo\": 23}", 0), std::exception, isDepthLimit); + BOOST_CHECK(ExpectLimitExceeded("[]", 0, "root")); + BOOST_CHECK(ExpectLimitExceeded("{}", 0, "root")); + BOOST_CHECK(ExpectLimitExceeded("[42]", 0, "root")); + BOOST_CHECK(ExpectLimitExceeded("{\"foo\": 23}", 0, "root")); // Array in array and object in object require at least depth limit 2. - BOOST_CHECK_EXCEPTION(JsonDecode("[[]]", 1), std::exception, isDepthLimit); + BOOST_CHECK(ExpectLimitExceeded("[[]]", 1, "root[0]")); BOOST_CHECK_NO_THROW(JsonDecode("[[]]", 2)); - BOOST_CHECK_EXCEPTION(JsonDecode(R"({"a":{}})", 1), std::exception, isDepthLimit); + BOOST_CHECK(ExpectLimitExceeded(R"({"a":{}})", 1, R"(root["a"])")); BOOST_CHECK_NO_THROW(JsonDecode(R"({"a":{}})", 2)); // Mixed nesting of arrays and objects is both counted towards the same limit. - BOOST_CHECK_EXCEPTION(JsonDecode("[{}]", 1), std::exception, isDepthLimit); + BOOST_CHECK(ExpectLimitExceeded("[{}]", 1, "root[0]")); BOOST_CHECK_NO_THROW(JsonDecode("[{}]", 2)); - BOOST_CHECK_EXCEPTION(JsonDecode(R"({"a":[]})", 1), std::exception, isDepthLimit); + BOOST_CHECK(ExpectLimitExceeded(R"({"a":[]})", 1, R"(root["a"])")); BOOST_CHECK_NO_THROW(JsonDecode(R"({"a":[]})", 2)); // Siblings are not added up for the depth check. @@ -204,12 +219,22 @@ BOOST_AUTO_TEST_CASE(decode_depth_limit) // Some deeper nested array. std::string arrayWithNesting42 = MakeNestedJsonArray(42); - BOOST_CHECK_EXCEPTION(JsonDecode(arrayWithNesting42, 41), std::exception, isDepthLimit); + std::ostringstream arrayPathWithNesting41; + arrayPathWithNesting41 << "root"; + for (size_t i = 0; i < 41; ++i) { + arrayPathWithNesting41 << "[0]"; + } + BOOST_CHECK(ExpectLimitExceeded(arrayWithNesting42, 41, arrayPathWithNesting41.str())); BOOST_CHECK_NO_THROW(JsonDecode(arrayWithNesting42, 42)); // Some deeper nested object. std::string objectWithNesting42 = MakeNestedJsonObject(42); - BOOST_CHECK_EXCEPTION(JsonDecode(objectWithNesting42, 41), std::exception, isDepthLimit); + std::ostringstream objectPathWithNesting41; + objectPathWithNesting41 << "root"; + for (size_t i = 0; i < 41; ++i) { + objectPathWithNesting41 << "[\"" << i << "\"]"; + } + BOOST_CHECK(ExpectLimitExceeded(objectWithNesting42, 41, objectPathWithNesting41.str())); BOOST_CHECK_NO_THROW(JsonDecode(objectWithNesting42, 42)); // And some deeper nested mixed containers. @@ -239,7 +264,8 @@ BOOST_AUTO_TEST_CASE(decode_depth_limit) } ] })"; - BOOST_CHECK_EXCEPTION(JsonDecode(deeperMixedNesting, 9), std::exception, isDepthLimit); + auto deeperPath = R"(root["1st-level"][2]["3rd-level"]["4th-level"][1]["6th-level"]["7th-level"]["8th-level"][1])"; + BOOST_CHECK(ExpectLimitExceeded(deeperMixedNesting, 9, deeperPath)); BOOST_CHECK_NO_THROW(JsonDecode(deeperMixedNesting, 10)); }