JsonDecode: include path in JSON depth error

If parsing JSON is rejected due to the depth limit introduced in the last
commit, also include the path (like root["object"]["children"]...) that exceeds
the allowed nesting depth.

(cherry picked from commit 14d6ee9cdf)
This commit is contained in:
Julian Brost 2026-05-29 17:14:20 +02:00
parent ed163a8aa9
commit b80132db87
2 changed files with 82 additions and 38 deletions

View file

@ -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<std::pair<Dictionary*, Array*>> m_CurrentSubtree;
String m_CurrentKey;
std::vector<Node> 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()));
}
}

View file

@ -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));
}