diff --git a/src/rpc/jsonrpc.cc b/src/rpc/jsonrpc.cc index 611c35aa..8f41ee1c 100644 --- a/src/rpc/jsonrpc.cc +++ b/src/rpc/jsonrpc.cc @@ -13,6 +13,7 @@ #include "rpc/command_map.h" #include "rpc/nlohmann/json.h" #include "rpc/parse_commands.h" +#include "rpc/scgi_task.h" #include "torrent/exceptions.h" #include "torrent/object.h" #include "utils/functional.h" @@ -264,6 +265,13 @@ JsonRpc::process(const char* in_buffer, uint32_t length, slot_write callback) { std::string response_str = response.dump(); + if (response_str.size() > SCgiTask::max_response_size) { + const auto& id = response.is_object() && response.contains("id") ? response["id"] : json(nullptr); + auto err_str = json_error(JSONRPC_INTERNAL_ERROR, "response size exceeds maximum RPC limit", id).dump(); + + return callback(err_str.c_str(), err_str.size()); + } + return callback(response_str.c_str(), response_str.size()); } catch (json::exception& e) { diff --git a/src/rpc/scgi_task.cc b/src/rpc/scgi_task.cc index f6dc8afa..c29e00a3 100644 --- a/src/rpc/scgi_task.cc +++ b/src/rpc/scgi_task.cc @@ -405,7 +405,7 @@ void SCgiTask::receive_write(const char* buffer, uint32_t length) { assert(torrent::this_thread::thread() == torrent::main_thread::thread()); - if (buffer == nullptr || length > (100 << 20)) + if (buffer == nullptr || length > max_response_size) throw torrent::internal_error("SCgiTask::receive_write(...) received bad input."); // Main thread callback already locked this mutex. diff --git a/src/rpc/scgi_task.h b/src/rpc/scgi_task.h index 22f93787..7b0ec874 100644 --- a/src/rpc/scgi_task.h +++ b/src/rpc/scgi_task.h @@ -2,6 +2,7 @@ #define RTORRENT_RPC_SCGI_TASK_H #include +#include #include #include #include @@ -18,6 +19,8 @@ public: static constexpr int max_header_size = 2000; static constexpr int max_content_size = (1 << 26); + static constexpr uint32_t max_response_size = (100 << 20); + static constexpr auto timeout_request = std::chrono::seconds(60); enum ContentType { XML, JSON }; diff --git a/src/rpc/xmlrpc_tinyxml2.cc b/src/rpc/xmlrpc_tinyxml2.cc index a7f28cf1..083da9ae 100644 --- a/src/rpc/xmlrpc_tinyxml2.cc +++ b/src/rpc/xmlrpc_tinyxml2.cc @@ -425,6 +425,14 @@ XmlRpc::process(const char* inBuffer, uint32_t length, slot_write slotWrite) { // remains. tinyxml2::XMLPrinter printer(nullptr, true, 0); process_document(&doc, &printer); + + if (printer.CStrSize() - 1 > static_cast(SCgiTask::max_response_size)) { + tinyxml2::XMLPrinter fault_printer(nullptr, true, 0); + print_xmlrpc_fault(XMLRPC_LIMIT_EXCEEDED_ERROR, "Response size exceeds maximum XML-RPC limit", &fault_printer); + + return slotWrite(fault_printer.CStr(), fault_printer.CStrSize() - 1); + } + return slotWrite(printer.CStr(), printer.CStrSize() - 1); } catch (rpc_error& e) { tinyxml2::XMLPrinter printer(nullptr, true, 0); diff --git a/test/rpc/test_jsonrpc.cc b/test/rpc/test_jsonrpc.cc index c8940e09..c5bec92c 100644 --- a/test/rpc/test_jsonrpc.cc +++ b/test/rpc/test_jsonrpc.cc @@ -8,12 +8,18 @@ #include "globals.h" #include "command_helpers.h" #include "rpc/command_map.h" +#include "rpc/scgi_task.h" CPPUNIT_TEST_SUITE_REGISTRATION(TestJsonrpc); torrent::Object jsonrpc_cmd_test_reflect([[maybe_unused]] rpc::target_type t, const torrent::Object& obj) { return obj; } +torrent::Object +jsonrpc_cmd_test_oversized([[maybe_unused]] rpc::target_type t, [[maybe_unused]] const torrent::Object& obj) { + return torrent::Object(std::string(rpc::SCgiTask::max_response_size + (1 << 20), 'a')); +} + void initialize_command_dynamic(); // Name, Request, Expected response @@ -130,6 +136,10 @@ TestJsonrpc::setUp() { if (rpc::commands.find("jsonrpc_reflect") == rpc::commands.end()) { CMD2_ANY("jsonrpc_reflect", &jsonrpc_cmd_test_reflect); } + + if (rpc::commands.find("jsonrpc_oversized") == rpc::commands.end()) { + CMD2_ANY("jsonrpc_oversized", &jsonrpc_cmd_test_oversized); + } } void @@ -145,3 +155,21 @@ TestJsonrpc::test_basics() { CPPUNIT_ASSERT_EQUAL_MESSAGE(std::get<0>(test), std::get<2>(test), output); } } + +// A command whose result does not fit in the SCGI response buffer must be +// answered with a fault, not handed to the writer. SCgiTask::receive_write +// treats an oversized body as an internal_error, which is not caught by any +// RPC handler and terminates the process. +void +TestJsonrpc::test_response_size_limit() { + const std::string request = R"({"jsonrpc": "2.0", "method": "jsonrpc_oversized", "params": [""], "id": 1})"; + const std::string expected = R"({"error":{"code":-32000,"message":"response size exceeds maximum RPC limit"},"id":1,"jsonrpc":"2.0"})"; + + std::string output; + m_jsonrpc.process(request.c_str(), request.size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; }); + + CPPUNIT_ASSERT_MESSAGE("response handed to the writer is " + std::to_string(output.size()) + + " bytes, over the " + std::to_string(rpc::SCgiTask::max_response_size) + " byte SCGI limit", + output.size() <= rpc::SCgiTask::max_response_size); + CPPUNIT_ASSERT_EQUAL(expected, output); +} diff --git a/test/rpc/test_jsonrpc.h b/test/rpc/test_jsonrpc.h index 132f5ef5..5e2638c5 100644 --- a/test/rpc/test_jsonrpc.h +++ b/test/rpc/test_jsonrpc.h @@ -8,6 +8,7 @@ class TestJsonrpc : public test_fixture { CPPUNIT_TEST_SUITE(TestJsonrpc); CPPUNIT_TEST(test_basics); + CPPUNIT_TEST(test_response_size_limit); CPPUNIT_TEST_SUITE_END(); @@ -16,6 +17,7 @@ public: void tearDown(); void test_basics(); + void test_response_size_limit(); private: std::unique_ptr m_test_main_thread; diff --git a/test/rpc/test_xmlrpc.cc b/test/rpc/test_xmlrpc.cc index b9557fc4..b3d68ead 100644 --- a/test/rpc/test_xmlrpc.cc +++ b/test/rpc/test_xmlrpc.cc @@ -8,6 +8,7 @@ #include "globals.h" #include "command_helpers.h" #include "rpc/command_map.h" +#include "rpc/scgi_task.h" CPPUNIT_TEST_SUITE_REGISTRATION(TestXmlrpc); @@ -21,6 +22,11 @@ xmlrpc_cmd_test_reflect_string([[maybe_unused]] rpc::target_type t, const std::s return obj; } +torrent::Object +xmlrpc_cmd_test_oversized([[maybe_unused]] rpc::target_type t, [[maybe_unused]] const torrent::Object& obj) { + return torrent::Object(std::string(rpc::SCgiTask::max_response_size + (1 << 20), 'a')); +} + void initialize_command_dynamic(); #if defined(HAVE_XMLRPC_TINYXML2) && !defined(HAVE_XMLRPC_C) @@ -127,6 +133,9 @@ TestXmlrpc::setUp() { if (rpc::commands.find("xmlrpc_reflect_string") == rpc::commands.end()) CMD2_ANY_STRING("xmlrpc_reflect_string", &xmlrpc_cmd_test_reflect_string); + + if (rpc::commands.find("xmlrpc_oversized") == rpc::commands.end()) + CMD2_ANY("xmlrpc_oversized", &xmlrpc_cmd_test_oversized); } void @@ -165,11 +174,30 @@ TestXmlrpc::test_size_limit() { CPPUNIT_ASSERT_EQUAL(expected, output); } +// A command whose result does not fit in the SCGI response buffer must be +// answered with a fault, not handed to the writer. SCgiTask::receive_write +// treats an oversized body as an internal_error, which is not caught by any +// RPC handler and terminates the process. +void +TestXmlrpc::test_response_size_limit() { + std::string input = "xmlrpc_oversized"; + std::string expected = "faultCode-509faultStringResponse size exceeds maximum XML-RPC limit"; + std::string output; + + m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;}); + + CPPUNIT_ASSERT_MESSAGE("response handed to the writer is " + std::to_string(output.size()) + + " bytes, over the " + std::to_string(rpc::SCgiTask::max_response_size) + " byte SCGI limit", + output.size() <= rpc::SCgiTask::max_response_size); + CPPUNIT_ASSERT_EQUAL(expected, output); +} + #else void TestXmlrpc::test_invalid_utf8() {} void TestXmlrpc::test_basics() {} void TestXmlrpc::test_size_limit() {} +void TestXmlrpc::test_response_size_limit() {} void TestXmlrpc::setUp() {} void TestXmlrpc::tearDown() {} diff --git a/test/rpc/test_xmlrpc.h b/test/rpc/test_xmlrpc.h index efa4382b..7e788b52 100644 --- a/test/rpc/test_xmlrpc.h +++ b/test/rpc/test_xmlrpc.h @@ -10,6 +10,7 @@ class TestXmlrpc : public test_fixture { CPPUNIT_TEST(test_basics); CPPUNIT_TEST(test_invalid_utf8); CPPUNIT_TEST(test_size_limit); + CPPUNIT_TEST(test_response_size_limit); CPPUNIT_TEST_SUITE_END(); @@ -22,6 +23,7 @@ public: void test_basics(); void test_invalid_utf8(); void test_size_limit(); + void test_response_size_limit(); private: std::unique_ptr m_test_main_thread;