mirror of
https://github.com/rakshasa/rtorrent.git
synced 2026-08-05 17:52:29 +00:00
Add untrusted connection security infrastructure (v3)
Replace the v2 blacklist approach with a per-command flag system. Commands must opt in to being available for untrusted connections via flag_untrusted_safe (0x400), checked in call_command() which catches all execution paths including nested commands. Infrastructure changes: - Add flag_untrusted_safe to CommandMap - Add untrusted_error exception type for proper error codes - Enforce trust check in both call_command() overloads - Add catch blocks in xmlrpc_c, xmlrpc_tinyxml2, and jsonrpc handlers - Port SCGI trust state management from v2 (thread_local, header parsing) - Add _U macro variants in command_helpers.h for safe command registration - Add CMD2_VAR_*_U and CMD2_VAR_*_U_GET variants for variables
This commit is contained in:
@@ -19,6 +19,11 @@ void initialize_commands();
|
||||
rpc::commands.insert_slot<rpc::command_base_is_type<rpc::function>::type>(key, slot, &rpc::function, \
|
||||
rpc::CommandMap::flag_dont_delete, NULL, NULL);
|
||||
|
||||
#define CMD2_A_FUNCTION_U(key, function, slot, parm, doc) \
|
||||
rpc::commands.insert_slot<rpc::command_base_is_type<rpc::function>::type>(key, slot, &rpc::function, \
|
||||
rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_rpc | \
|
||||
rpc::CommandMap::flag_untrusted_safe, NULL, NULL);
|
||||
|
||||
#define CMD2_ANY(key, slot) CMD2_A_FUNCTION(key, command_base_call<rpc::target_type>, slot, "i:", "")
|
||||
|
||||
#define CMD2_ANY_P(key, slot) CMD2_A_FUNCTION_PRIVATE(key, command_base_call<rpc::target_type>, slot, "i:", "")
|
||||
@@ -35,6 +40,42 @@ void initialize_commands();
|
||||
|
||||
#define CMD2_ANY_LIST(key, slot) CMD2_A_FUNCTION(key, command_base_call_list<rpc::target_type>, slot, "i:", "")
|
||||
|
||||
#define CMD2_ANY_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<rpc::target_type>, slot, "i:", "")
|
||||
#define CMD2_ANY_VOID_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<rpc::target_type>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_ANY_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list<rpc::target_type>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_ANY_L_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list<rpc::target_type>, slot, "A:", "")
|
||||
|
||||
#define CMD2_ANY_VALUE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<rpc::target_type>, slot, "i:i", "")
|
||||
#define CMD2_ANY_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<rpc::target_type>, object_convert_void(slot), "i:i", "")
|
||||
#define CMD2_ANY_VALUE_KB_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value_kb<rpc::target_type>, object_convert_void(slot), "i:i", "")
|
||||
|
||||
#define CMD2_ANY_STRING_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string<rpc::target_type>, slot, "i:s", "")
|
||||
#define CMD2_ANY_STRING_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string<rpc::target_type>, object_convert_void(slot), "i:s", "")
|
||||
|
||||
#define CMD2_ANY_LIST_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list<rpc::target_type>, slot, "i:", "")
|
||||
|
||||
#define CMD2_DL_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<core::Download*>, slot, "i:", "")
|
||||
#define CMD2_DL_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<core::Download*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_DL_VALUE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<core::Download*>, slot, "i:", "")
|
||||
#define CMD2_DL_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<core::Download*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_DL_STRING_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string<core::Download*>, slot, "i:", "")
|
||||
#define CMD2_DL_STRING_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string<core::Download*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_DL_LIST_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list<core::Download*>, slot, "i:", "")
|
||||
|
||||
#define CMD2_FILE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::File*>, slot, "i:", "")
|
||||
#define CMD2_FILE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::File*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_FILE_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<torrent::File*>, object_convert_void(slot), "i:i", "")
|
||||
|
||||
#define CMD2_FILEITR_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::FileListIterator*>, slot, "i:", "")
|
||||
|
||||
#define CMD2_PEER_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::Peer*>, slot, "i:", "")
|
||||
#define CMD2_PEER_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::Peer*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_PEER_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<torrent::Peer*>, object_convert_void(slot), "i:i", "")
|
||||
|
||||
#define CMD2_TRACKER_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::tracker::Tracker*>, slot, "i:", "")
|
||||
#define CMD2_TRACKER_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call<torrent::tracker::Tracker*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_TRACKER_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value<torrent::tracker::Tracker*>, object_convert_void(slot), "i:i", "")
|
||||
|
||||
#define CMD2_DL(key, slot) CMD2_A_FUNCTION(key, command_base_call<core::Download*>, slot, "i:", "")
|
||||
#define CMD2_DL_V(key, slot) CMD2_A_FUNCTION(key, command_base_call<core::Download*>, object_convert_void(slot), "i:", "")
|
||||
#define CMD2_DL_VALUE(key, slot) CMD2_A_FUNCTION(key, command_base_call_value<core::Download*>, slot, "i:", "")
|
||||
@@ -96,6 +137,71 @@ void initialize_commands();
|
||||
CMD2_ANY_VOID(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_BOOL_U(key, value) \
|
||||
control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_bool_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_VALUE_U(key ".set", std::bind(&rpc::object_storage::set_bool, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_BOOL_U_GET(key, value) \
|
||||
control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_bool_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_VALUE(key ".set", std::bind(&rpc::object_storage::set_bool, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_VALUE_U(key, value) \
|
||||
control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_value_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_VALUE_U(key ".set", std::bind(&rpc::object_storage::set_value, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_VALUE_U_GET(key, value) \
|
||||
control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_value_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_VALUE(key ".set", std::bind(&rpc::object_storage::set_value, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_STRING_U(key, value) \
|
||||
control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_STRING_U(key ".set", std::bind(&rpc::object_storage::set_string, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_STRING_U_GET(key, value) \
|
||||
control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_STRING(key ".set", std::bind(&rpc::object_storage::set_string, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_C_STRING_U(key, value) \
|
||||
control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key)));
|
||||
|
||||
#define CMD2_VAR_LIST_U(key) \
|
||||
control->object_storage()->insert_c_str(key, torrent::Object::create_list(), rpc::object_storage::flag_list_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_LIST_U(key ".set", std::bind(&rpc::object_storage::set_list, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2)); \
|
||||
CMD2_ANY_VOID_U(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_VAR_LIST_U_GET(key) \
|
||||
control->object_storage()->insert_c_str(key, torrent::Object::create_list(), rpc::object_storage::flag_list_type); \
|
||||
CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key))); \
|
||||
CMD2_ANY_LIST(key ".set", std::bind(&rpc::object_storage::set_list, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2)); \
|
||||
CMD2_ANY_VOID(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \
|
||||
torrent::raw_string::from_c_str(key), std::placeholders::_2));
|
||||
|
||||
#define CMD2_FUNC_SINGLE(key, cmds) \
|
||||
CMD2_ANY(key, std::bind(&rpc::command_function_call_object, torrent::Object(torrent::raw_string::from_c_str(cmds)), \
|
||||
std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
@@ -98,11 +98,17 @@ CommandMap::call_command(const key_type& key, const mapped_type& arg, const targ
|
||||
if (itr == base_type::end())
|
||||
throw torrent::input_error("Command \"" + std::string(key) + "\" does not exist.");
|
||||
|
||||
if (!RpcManager::is_trusted() && !(itr->second.m_flags & flag_untrusted_safe))
|
||||
throw untrusted_error("Command \"" + std::string(key) + "\" is not allowed for untrusted connections.");
|
||||
|
||||
return itr->second.m_anySlot(&itr->second.m_variable, target, arg);
|
||||
}
|
||||
|
||||
const CommandMap::mapped_type
|
||||
CommandMap::call_command(iterator itr, const mapped_type& arg, const target_type& target) {
|
||||
if (!RpcManager::is_trusted() && !(itr->second.m_flags & flag_untrusted_safe))
|
||||
throw untrusted_error("Command \"" + itr->first + "\" is not allowed for untrusted connections.");
|
||||
|
||||
return itr->second.m_anySlot(&itr->second.m_variable, target, arg);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public:
|
||||
static const int flag_file_target = 0x100;
|
||||
static const int flag_tracker_target = 0x200;
|
||||
|
||||
static const int flag_untrusted_safe = 0x400;
|
||||
|
||||
CommandMap() = default;
|
||||
|
||||
bool has(const std::string& key) const { return base_type::find(key) != base_type::end(); }
|
||||
|
||||
+6
-3
@@ -133,9 +133,12 @@ jsonrpc_call_command(const std::string& method, const json& params) {
|
||||
|
||||
params_object_list.erase(params_object_list.begin());
|
||||
|
||||
const auto& result = rpc::commands.call_command(itr, params_object, target);
|
||||
|
||||
return object_to_json(result);
|
||||
try {
|
||||
const auto& result = rpc::commands.call_command(itr, params_object, target);
|
||||
return object_to_json(result);
|
||||
} catch (untrusted_error& e) {
|
||||
throw rpc_error(JSONRPC_METHOD_NOT_FOUND_ERROR, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
json
|
||||
|
||||
@@ -13,6 +13,28 @@ CommandMap commands;
|
||||
RpcManager rpc;
|
||||
ExecFile execFile;
|
||||
|
||||
// Trusted/untrusted XMLRPC connection model.
|
||||
//
|
||||
// The trust state is set per-request by the SCGI layer based on the
|
||||
// UNTRUSTED_CONNECTION header. Commands without flag_untrusted_safe
|
||||
// are blocked for untrusted connections. The check is in
|
||||
// CommandMap::call_command(), which catches all command execution
|
||||
// including nested calls through argument expansion.
|
||||
|
||||
thread_local bool RpcManager::m_trusted = true;
|
||||
|
||||
bool
|
||||
RpcManager::set_trusted(bool trusted) {
|
||||
bool prev = m_trusted;
|
||||
m_trusted = trusted;
|
||||
return prev;
|
||||
}
|
||||
|
||||
bool
|
||||
RpcManager::is_trusted() {
|
||||
return m_trusted;
|
||||
}
|
||||
|
||||
void
|
||||
RpcManager::object_to_target(const torrent::Object& obj, int call_flags, rpc::target_type* target, std::function<void()>* deleter) {
|
||||
if (!obj.is_string())
|
||||
|
||||
@@ -34,6 +34,17 @@ private:
|
||||
std::string m_msg;
|
||||
};
|
||||
|
||||
class untrusted_error : public torrent::base_error {
|
||||
public:
|
||||
untrusted_error(std::string msg) : m_msg(std::move(msg)) {}
|
||||
virtual ~untrusted_error() throw() = default;
|
||||
|
||||
virtual const char* what() const throw() { return m_msg.c_str(); }
|
||||
|
||||
private:
|
||||
std::string m_msg;
|
||||
};
|
||||
|
||||
class RpcManager {
|
||||
public:
|
||||
using slot_download = std::function<core::Download*(const char*)>;
|
||||
@@ -71,9 +82,17 @@ public:
|
||||
slot_tracker& slot_find_tracker() { return m_slot_find_tracker; }
|
||||
slot_peer& slot_find_peer() { return m_slot_find_peer; }
|
||||
|
||||
// Trusted/untrusted XMLRPC connection model.
|
||||
// When an SCGI request includes the UNTRUSTED_CONNECTION header,
|
||||
// commands without flag_untrusted_safe are blocked.
|
||||
static bool set_trusted(bool trusted);
|
||||
static bool is_trusted();
|
||||
|
||||
static void object_to_target(const torrent::Object& obj, int callFlags, rpc::target_type* target, std::function<void()>* deleter);
|
||||
|
||||
private:
|
||||
static thread_local bool m_trusted;
|
||||
|
||||
XmlRpc m_xmlrpc;
|
||||
JsonRpc m_jsonrpc;
|
||||
|
||||
|
||||
+27
-16
@@ -111,6 +111,9 @@ SCgiTask::event_read() {
|
||||
size_t content_length = 0;
|
||||
const char* header_end = current + header_size;
|
||||
|
||||
// Assume trusted until we find the UNTRUSTED_CONNECTION header.
|
||||
m_trusted = true;
|
||||
|
||||
// Parse out the null-terminated header keys and values, with
|
||||
// checks to ensure it doesn't scan beyond the limits of the
|
||||
// header
|
||||
@@ -141,6 +144,8 @@ SCgiTask::event_read() {
|
||||
goto event_read_failed;
|
||||
} else if (strcmp(key, "CONTENT_TYPE") == 0) {
|
||||
content_type = value;
|
||||
} else if (strcmp(key, "UNTRUSTED_CONNECTION") == 0 && strcmp(value, "1") == 0) {
|
||||
m_trusted = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +275,7 @@ SCgiTask::receive_call(const char* buffer, uint32_t length) {
|
||||
// TODO: Rewrite RpcManager.process to pass the result buffer instead of having to copy it.
|
||||
|
||||
auto scgi_thread = torrent::utils::Thread::self();
|
||||
bool trusted = m_trusted;
|
||||
|
||||
auto result_callback = [this, scgi_thread](const char* b, uint32_t l) {
|
||||
receive_write(b, l);
|
||||
@@ -285,30 +291,35 @@ SCgiTask::receive_call(const char* buffer, uint32_t length) {
|
||||
|
||||
auto lock = std::lock_guard<std::mutex>(m_result_mutex);
|
||||
|
||||
RpcManager::RPCType rpc_type;
|
||||
|
||||
switch (content_type()) {
|
||||
case rpc::SCgiTask::ContentType::JSON:
|
||||
torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback]() {
|
||||
rpc.process(RpcManager::RPCType::JSON, buffer, length,
|
||||
[result_callback](const char* b, uint32_t l) {
|
||||
result_callback(b, l);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
rpc_type = RpcManager::RPCType::JSON;
|
||||
break;
|
||||
|
||||
case rpc::SCgiTask::ContentType::XML:
|
||||
torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback]() {
|
||||
rpc.process(RpcManager::RPCType::XML, buffer, length,
|
||||
[result_callback](const char* b, uint32_t l) {
|
||||
result_callback(b, l);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
rpc_type = RpcManager::RPCType::XML;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw torrent::internal_error("SCgiTask::receive_call(...) received bad input.");
|
||||
}
|
||||
|
||||
torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback, trusted, rpc_type]() {
|
||||
rpc::RpcManager::set_trusted(trusted);
|
||||
|
||||
try {
|
||||
rpc.process(rpc_type, buffer, length,
|
||||
[result_callback](const char* b, uint32_t l) {
|
||||
result_callback(b, l);
|
||||
return true;
|
||||
});
|
||||
} catch (...) {
|
||||
rpc::RpcManager::set_trusted(true);
|
||||
throw;
|
||||
}
|
||||
|
||||
rpc::RpcManager::set_trusted(true);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -53,6 +53,7 @@ private:
|
||||
unsigned int m_buffer_size{0};
|
||||
|
||||
ContentType m_content_type{ XML };
|
||||
bool m_trusted{true};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -385,6 +385,10 @@ xmlrpc_call_command(xmlrpc_env* env, xmlrpc_value* args, void* voidServerInfo) {
|
||||
|
||||
return object_to_xmlrpc(env, rpc::commands.call_command(itr, object, target));
|
||||
|
||||
} catch (untrusted_error& e) {
|
||||
xmlrpc_env_set_fault(env, XMLRPC_REQUEST_REFUSED_ERROR, e.what());
|
||||
return NULL;
|
||||
|
||||
} catch (xmlrpc_error_c& e) {
|
||||
xmlrpc_env_set_fault(env, e.type(), e.what());
|
||||
return NULL;
|
||||
|
||||
@@ -31,7 +31,7 @@ const int XMLRPC_PARSE_ERROR = -503;
|
||||
// const int XMLRPC_NETWORK_ERROR = -504;
|
||||
// const int XMLRPC_TIMEOUT_ERROR = -505;
|
||||
const int XMLRPC_NO_SUCH_METHOD_ERROR = -506;
|
||||
// const int XMLRPC_REQUEST_REFUSED_ERROR = -507;
|
||||
const int XMLRPC_REQUEST_REFUSED_ERROR = -507;
|
||||
// const int XMLRPC_INTROSPECTION_DISABLED_ERROR = -508;
|
||||
const int XMLRPC_LIMIT_EXCEEDED_ERROR = -509;
|
||||
// const int XMLRPC_INVALID_UTF8_ERROR = -510;
|
||||
@@ -238,7 +238,11 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
|
||||
throw rpc_error(XMLRPC_TYPE_ERROR, "invalid parameters: too few");
|
||||
}
|
||||
|
||||
return rpc::commands.call_command(cmd_itr, params_raw, target);
|
||||
try {
|
||||
return rpc::commands.call_command(cmd_itr, params_raw, target);
|
||||
} catch (untrusted_error& e) {
|
||||
throw rpc_error(XMLRPC_REQUEST_REFUSED_ERROR, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user