Bound JSON-RPC input by size and nesting depth

Enforce the nesting bound in one parse, capping allocation by depth, not input size.
This commit is contained in:
xirvik
2026-09-19 02:11:20 +00:00
committed by Jari Sundell
parent 83b03c2c1e
commit 439d23ce62
5 changed files with 130 additions and 1 deletions
+79
View File
@@ -173,3 +173,82 @@ TestJsonrpc::test_response_size_limit() {
output.size() <= rpc::SCgiTask::max_response_size);
CPPUNIT_ASSERT_EQUAL(expected, output);
}
// The bound is applied while the document is parsed: json_to_object only ever
// walks "params", and by the time it runs the whole tree already exists.
void
TestJsonrpc::test_depth_limit() {
// A JSON string holding a quote and brackets that must not be counted.
const std::string tricky = R"("\"[[[")";
std::vector<std::tuple<std::string, std::string, std::string>> requests = {
std::make_tuple("Nesting under the limit is accepted",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
std::string(1000, '[') + std::string(1000, ']') + R"(], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[)" +
std::string(1000, '[') + std::string(1000, ']') + R"(]})"),
// Nesting outside "params" is never converted, so json_to_object's own
// bound never sees it.
std::make_tuple("Nesting outside params is rejected",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1, "x": )" +
std::string(2000, '[') + std::string(2000, ']') + R"(})",
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
std::make_tuple("Nesting over the limit is rejected",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
std::string(2000, '[') + std::string(2000, ']') + R"(], "id": 1})",
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
std::make_tuple("Brackets inside a string are not nesting",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", ")" +
std::string(2000, '[') + R"("], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[")" +
std::string(2000, '[') + R"("]})"),
std::make_tuple("An escaped quote does not end a string",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" + tricky + R"(, ")" +
std::string(2000, '[') + R"("], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[)" + tricky + R"(,")" +
std::string(2000, '[') + R"("]})"),
// The bound is on the whole document, so the outer object and the params
// array are two of the 1024 containers and 1022 are left for the payload.
std::make_tuple("Nesting one below the limit is accepted",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
std::string(1021, '[') + std::string(1021, ']') + R"(], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[)" +
std::string(1021, '[') + std::string(1021, ']') + R"(]})"),
std::make_tuple("Nesting at the limit is accepted",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
std::string(1022, '[') + std::string(1022, ']') + R"(], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[)" +
std::string(1022, '[') + std::string(1022, ']') + R"(]})"),
std::make_tuple("Nesting one over the limit is rejected",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
std::string(1023, '[') + std::string(1023, ']') + R"(], "id": 1})",
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
};
for (auto& test : requests) {
std::string output;
m_jsonrpc.process(std::get<1>(test).c_str(), std::get<1>(test).size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; });
CPPUNIT_ASSERT_EQUAL_MESSAGE(std::get<0>(test), std::get<2>(test), output);
}
}
// network.xmlrpc.size_limit is the only knob bounding how much input a single
// request may spend memory on, and it has to bound the JSON path too.
void
TestJsonrpc::test_size_limit() {
const std::string request = R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1})";
const std::string expected = R"({"error":{"code":-32600,"message":"content size exceeds maximum RPC limit"},"id":null,"jsonrpc":"2.0"})";
std::string output;
m_jsonrpc.set_size_limit(1);
m_jsonrpc.process(request.c_str(), request.size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; });
CPPUNIT_ASSERT_EQUAL(expected, output);
}