fix(libmsg): complete security audit round 2 - fix P0 race + harden input validation
- P0: fix _close_all_clients() race condition (CWE-362) - hold per-connection send_lock before close() to prevent messages being sent to wrong client after fd reuse - P2: add comprehensive input validation for all public APIs (timeout/port/workers/max_connections/batch_size etc. bounds checking) - P2: add UTF-8 payload validation (isValidUtf8) aligning with Python pymsg behavior, rejecting overlong encodings, surrogate pairs, truncated/invalid sequences - P2: document pmsg_recv(int) precondition and recommend deadline mode - add constructor validation for MsgClient/MsgServer/AsyncQueue/RpcClient/RpcServer - add add_route() validation for empty route name and null handler - update security audit report with round 2 findings and fixes (8 issues total, 100% fixed)
This commit is contained in:
1 parent
9256a8157b
commit
3c50a862d3
6 files changed
+351
-76
No files matched your search
@@ -41,6 +41,8 @@
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -87,7 +89,20 @@ public:
|
||||
_running(false),
|
||||
_stop(false),
|
||||
_wakeup(false),
|
||||
_dropped(0) {}
|
||||
_dropped(0) {
|
||||
if (!_handler) {
|
||||
throw std::invalid_argument("AsyncQueue: handler must not be null");
|
||||
}
|
||||
if (config.batch_size == 0) {
|
||||
throw std::invalid_argument("AsyncQueue: batch_size must be > 0");
|
||||
}
|
||||
if (config.flush_interval <= 0 || config.flush_interval > 300.0) {
|
||||
throw std::invalid_argument("AsyncQueue: flush_interval must be in (0, 300] seconds");
|
||||
}
|
||||
if (config.poll_timeout <= 0 || config.poll_timeout > 5.0) {
|
||||
throw std::invalid_argument("AsyncQueue: poll_timeout must be in (0, 5] seconds");
|
||||
}
|
||||
}
|
||||
|
||||
/// 析构函数:自动 shutdown(),保证队列中剩余数据被 flush。
|
||||
~AsyncQueue() { shutdown(); }
|
||||
|
||||
+77
-3
@@ -51,6 +51,59 @@ namespace detail {
|
||||
|
||||
using namespace msg_constants;
|
||||
|
||||
// ---- UTF-8 合法性校验(对齐 Python decode('utf-8'))------------------------
|
||||
// 防御性校验:协议声明载荷为 UTF-8,非法字节序列表示协议错误或攻击,
|
||||
// 直接抛 MsgConnectionException 断开连接。
|
||||
inline bool isValidUtf8(const char* data, size_t len) noexcept {
|
||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data);
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
unsigned char c = bytes[i];
|
||||
int num_extra;
|
||||
uint32_t codepoint;
|
||||
if (c <= 0x7F) {
|
||||
++i;
|
||||
continue;
|
||||
} else if ((c & 0xE0) == 0xC0) {
|
||||
num_extra = 1;
|
||||
codepoint = c & 0x1F;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
num_extra = 2;
|
||||
codepoint = c & 0x0F;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
num_extra = 3;
|
||||
codepoint = c & 0x07;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (i + num_extra >= len) return false;
|
||||
for (int j = 0; j < num_extra; ++j) {
|
||||
++i;
|
||||
if ((bytes[i] & 0xC0) != 0x80) return false;
|
||||
codepoint = (codepoint << 6) | (bytes[i] & 0x3F);
|
||||
}
|
||||
++i;
|
||||
// 拒绝 overlong 编码、代理对、超 U+10FFFF
|
||||
if (num_extra == 1 && codepoint < 0x80) return false; // 2字节序列应>=U+0080
|
||||
if (num_extra == 2 && (codepoint < 0x800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF))) return false; // 3字节序列>=U+0800且非代理对
|
||||
if (num_extra == 3 && (codepoint < 0x10000 || codepoint > 0x10FFFF)) return false; // 4字节序列U+10000~U+10FFFF
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- 参数校验辅助 --------------------------------------------------------
|
||||
inline void validateTimeout(double timeout_sec) {
|
||||
if (timeout_sec <= 0 || timeout_sec > 300.0) { // 最大 5 分钟
|
||||
throw MsgException("invalid timeout: " + std::to_string(timeout_sec) + " seconds (must be in (0, 300])");
|
||||
}
|
||||
}
|
||||
|
||||
inline void validatePort(int port) {
|
||||
if (port < 1 || port > 65535) {
|
||||
throw MsgConnectionException("invalid port: " + std::to_string(port) + " (must be in [1, 65535])");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 4-byte big-endian 编码 / 解码 ----------------------------------------
|
||||
|
||||
inline void writeUint32BE(char *buf, uint32_t val) noexcept {
|
||||
@@ -149,6 +202,7 @@ inline void recvExactDeadline(int sockfd, char *buf, size_t n,
|
||||
|
||||
// deadline 模式接收完整 pymsg 帧(header + payload)
|
||||
inline std::string recvFrameDeadline(int sockfd, double timeout_sec) {
|
||||
validateTimeout(timeout_sec);
|
||||
const auto deadline = std::chrono::steady_clock::now()
|
||||
+ std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
std::chrono::duration<double>(timeout_sec));
|
||||
@@ -163,6 +217,10 @@ inline std::string recvFrameDeadline(int sockfd, double timeout_sec) {
|
||||
std::string payload(static_cast<size_t>(length), '\0');
|
||||
if (length > 0) {
|
||||
recvExactDeadline(sockfd, payload.data(), length, deadline);
|
||||
// UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
|
||||
if (!isValidUtf8(payload.data(), length)) {
|
||||
throw MsgConnectionException("invalid UTF-8 payload");
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
@@ -188,6 +246,8 @@ inline void sendAll(int sockfd, const char *data, size_t len) {
|
||||
// ---- 短连接 helper:connect → action → close ------------------------------
|
||||
|
||||
inline int openConnection(const std::string& addr, int port, double timeout) {
|
||||
validatePort(port);
|
||||
validateTimeout(timeout);
|
||||
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
throw MsgConnectionException(
|
||||
@@ -240,7 +300,9 @@ inline void pmsg_send(int sockfd, const std::string &data) {
|
||||
}
|
||||
|
||||
/// 以 pymsg 协议接收一条字符串消息(阻塞模式,依赖 socket 已设置的超时)。
|
||||
/// @throws MsgConnectionException 连接在收全消息前断开。
|
||||
/// 注意:调用方必须预先通过 setsockopt(SO_RCVTIMEO) 设置接收超时,否则可能永久阻塞。
|
||||
/// 推荐使用 pmsg_recv(sockfd, timeout_sec) deadline 模式,不依赖外部状态。
|
||||
/// @throws MsgConnectionException 连接在收全消息前断开、或载荷不是合法 UTF-8。
|
||||
/// @throws MsgTimeoutException 等待超时。
|
||||
inline std::string pmsg_recv(int sockfd) {
|
||||
char header[detail::kHeaderSize];
|
||||
@@ -250,7 +312,13 @@ inline std::string pmsg_recv(int sockfd) {
|
||||
throw MsgException("message too large: " + std::to_string(length));
|
||||
}
|
||||
std::string payload(static_cast<size_t>(length), '\0');
|
||||
detail::recvExact(sockfd, payload.data(), length);
|
||||
if (length > 0) {
|
||||
detail::recvExact(sockfd, payload.data(), length);
|
||||
// UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
|
||||
if (!detail::isValidUtf8(payload.data(), length)) {
|
||||
throw MsgConnectionException("invalid UTF-8 payload");
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -277,7 +345,9 @@ inline std::string pmsg_recv(int sockfd, double timeout_sec) {
|
||||
class MsgClient : public Msg_Basic {
|
||||
public:
|
||||
MsgClient(const std::string &addr, int port, const std::string &name)
|
||||
: _addr(addr), _port(port), _name(name), _sockfd(-1), _running(false) {}
|
||||
: _addr(addr), _port(port), _name(name), _sockfd(-1), _running(false) {
|
||||
detail::validatePort(port);
|
||||
}
|
||||
|
||||
~MsgClient() { stop(); }
|
||||
|
||||
@@ -325,6 +395,10 @@ public:
|
||||
std::string call(const std::string &msg, double timeout = static_cast<double>(msg_constants::kDefaultTimeout),
|
||||
bool retry_on_refused = false,
|
||||
double retry_interval = 1.0) {
|
||||
detail::validateTimeout(timeout);
|
||||
if (retry_interval <= 0 || retry_interval > 60.0) {
|
||||
throw MsgException("invalid retry_interval: " + std::to_string(retry_interval) + " seconds (must be in (0, 60])");
|
||||
}
|
||||
if (!retry_on_refused) {
|
||||
// 快速路径:不重试,直接执行一次
|
||||
int fd = detail::openConnection(_addr, _port, timeout);
|
||||
|
||||
@@ -86,7 +86,25 @@ public:
|
||||
using MessageCallback = std::function<void(int sockfd, const std::string& addr, const std::string& message)>;
|
||||
|
||||
explicit MsgServer(const std::string& addr, int port, MsgServerConfig config = {})
|
||||
: _addr(addr), _port(port), _config(config), _server_fd(-1), _running(false) {}
|
||||
: _addr(addr), _port(port), _config(config), _server_fd(-1), _running(false) {
|
||||
detail::validatePort(port);
|
||||
if (config.workers < 0 || config.workers > 256) {
|
||||
throw MsgException("invalid workers: " + std::to_string(config.workers) + " (must be in [0, 256])");
|
||||
}
|
||||
detail::validateTimeout(config.recv_timeout);
|
||||
if (config.max_connections < 1 || config.max_connections > 100000) {
|
||||
throw MsgException("invalid max_connections: " + std::to_string(config.max_connections));
|
||||
}
|
||||
if (config.max_queue_size < 1 || config.max_queue_size > 1000000) {
|
||||
throw MsgException("invalid max_queue_size: " + std::to_string(config.max_queue_size));
|
||||
}
|
||||
if (config.listen_backlog < 1 || config.listen_backlog > 4096) {
|
||||
throw MsgException("invalid listen_backlog: " + std::to_string(config.listen_backlog));
|
||||
}
|
||||
if (config.poll_timeout <= 0 || config.poll_timeout > 5.0) {
|
||||
throw MsgException("invalid poll_timeout: " + std::to_string(config.poll_timeout));
|
||||
}
|
||||
}
|
||||
|
||||
~MsgServer() { stop(); }
|
||||
|
||||
@@ -510,12 +528,33 @@ private:
|
||||
}
|
||||
|
||||
void _close_all_clients() {
|
||||
std::lock_guard<std::mutex> clk(_clients_mtx);
|
||||
for (auto& [fd, addr] : _clients) {
|
||||
::close(fd);
|
||||
std::vector<std::pair<int, std::shared_ptr<std::mutex>>> to_close;
|
||||
{
|
||||
std::lock_guard<std::mutex> clk(_clients_mtx);
|
||||
to_close.reserve(_clients.size());
|
||||
for (auto& [fd, addr] : _clients) {
|
||||
auto it = _send_locks.find(fd);
|
||||
if (it != _send_locks.end()) {
|
||||
to_close.emplace_back(fd, it->second);
|
||||
} else {
|
||||
to_close.emplace_back(fd, nullptr);
|
||||
}
|
||||
}
|
||||
_clients.clear();
|
||||
_send_locks.clear();
|
||||
}
|
||||
// 持有每个连接的 send_lock 后再 close(fd):防止 stop()/析构时
|
||||
// reply/broadcast 正在发送,导致数据发到 OS 复用的新连接(CWE-362)
|
||||
for (auto& [fd, send_lock] : to_close) {
|
||||
if (send_lock) {
|
||||
std::lock_guard<std::mutex> slk(*send_lock);
|
||||
::shutdown(fd, SHUT_RDWR);
|
||||
::close(fd);
|
||||
} else {
|
||||
::shutdown(fd, SHUT_RDWR);
|
||||
::close(fd);
|
||||
}
|
||||
}
|
||||
_clients.clear();
|
||||
_send_locks.clear();
|
||||
std::lock_guard<std::mutex> blk(_busy_mtx);
|
||||
_busy.clear();
|
||||
std::lock_guard<std::mutex> flk(_failed_mtx);
|
||||
|
||||
@@ -74,7 +74,14 @@ public:
|
||||
: _addr(addr), _port(port), _name(name),
|
||||
_timeout(timeout),
|
||||
_retry_on_refused(retry_on_refused),
|
||||
_retry_interval(retry_interval) {}
|
||||
_retry_interval(retry_interval) {
|
||||
if (timeout <= 0 || timeout > 300.0) {
|
||||
throw MsgException("invalid timeout: " + std::to_string(timeout));
|
||||
}
|
||||
if (retry_interval <= 0 || retry_interval > 60.0) {
|
||||
throw MsgException("invalid retry_interval: " + std::to_string(retry_interval));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RPC 调用 ----------------------------------------------------------
|
||||
|
||||
|
||||
@@ -80,10 +80,16 @@ public:
|
||||
|
||||
/** 注册路由处理函数。
|
||||
*
|
||||
* @param name 路由名(对应请求中的 _route 字段)
|
||||
* @param name 路由名(对应请求中的 _route 字段),不能为空
|
||||
* @param handler 处理函数,签名 json::value(const json::object& req)
|
||||
*/
|
||||
void add_route(const std::string& name, Handler handler) {
|
||||
if (name.empty()) {
|
||||
throw MsgException("route name cannot be empty");
|
||||
}
|
||||
if (!handler) {
|
||||
throw MsgException("route handler cannot be null");
|
||||
}
|
||||
_routes[name] = std::move(handler);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user