diff --git a/docs/security/libhttp_SECURITY_AUDIT_20260803.md b/docs/security/libhttp_SECURITY_AUDIT_20260803.md index d816b38..61d86d1 100644 --- a/docs/security/libhttp_SECURITY_AUDIT_20260803.md +++ b/docs/security/libhttp_SECURITY_AUDIT_20260803.md @@ -11,9 +11,9 @@ | 优先级 | 总数 | 已修复 | 部分修复 | 未修复 | |--------|------|--------|----------|--------| | P0 | 0 | 0 | 0 | 0 | -| P1 | 6 | 5 | 0 | 1 | +| P1 | 6 | 6 | 0 | 0 | | P2 | 4 | 0 | 0 | 4 | -| **合计** | **10** | **7** | **0** | **3** | +| **合计** | **10** | **8** | **0** | **2** | --- @@ -26,7 +26,7 @@ | P1-3 | WebSocket 消息无大小限制 | ✅ 已修复 | `HttpServer` 新增 `max_ws_message_size_`(默认 16MB,与 Beast 默认一致)与 `setMaxWsMessageSize()`/`getMaxWsMessageSize()`。`doWsUpgrade`/`doWssUpgrade` 及代理流 `onProxyConnect`/`onProxySslHandshake` 创建后调用 `read_message_max()`,超限消息由 Beast 在分配内存前拒绝并关闭连接,防 OOM | | P1-4 | 服务器无读写超时(Slowloris) | ✅ 已修复 | 入站 `stream_` 现设置超时:`doSSLHandshake` 设头超时(覆盖握手),`doRead` 两阶段读(`async_read_header` 头超时 → `async_read` 体超时),`doWsRead` 每个 WS 读取设空闲超时。`HttpServer` 新增 `setHeaderTimeout()`(默认 10s)/`setReadTimeout()`(默认 30s),对齐 WAF ReadHeaderTimeout/ReadTimeout。慢速/空闲连接被清理,防 Slowloris/连接耗尽 | | P1-5 | 静态文件目录路径遍历 | ✅ 已修复 | 新增 `isSafeStaticPath()`(`HttpServer_Request.cpp` L139-156),静态文件服务前校验 target_path(拒绝 `..`、空字节、非 `/` 开头、URL 编码 `%2e`/`%2f`/`%5c`),逻辑与 gateway `is_safe_target()` 对齐(`HttpServer.cpp` L94) | -| P1-6 | 发送队列无界(慢消费者 OOM) | ❌ 未修复 | `ws_send_queue_`、`proxy_to_backend_queue_`、`proxy_to_client_queue_`(`HttpServer_WsSession.cpp` L8-16/L362-367/L438-443)均为无界 `std::deque`。若客户端消费慢,服务端持续入队可导致内存耗尽 | +| P1-6 | 发送队列无界(慢消费者 OOM) | ✅ 已修复 | `HttpServer` 新增 `setMaxWsQueueSize()`(默认 1000),`wsSend` / `onProxyReadClient` / `onProxyReadBackend` 入队前检查,超过上限判定慢消费者/慢后端,关闭连接并清空队列,防止内存耗尽 | --- @@ -74,7 +74,7 @@ ### 短期(72小时) - [x] P1-3 WS 消息大小限制:设置 `read_message_max`(可配置)→ ✅ 已实现(`setMaxWsMessageSize()`,默认 16MB,服务端与代理流均生效) - [x] P1-5 静态文件路径遍历:新增 `isSafeStaticPath()` 拼接前校验 `target_path`,拒绝 `..`/空字节/编码遍历 → ✅ 已实现(`HttpServer_Request.cpp` L139-157) -- [ ] P1-6 发送队列上限:`ws_send_queue_`/代理队列设最大值,超限断开慢消费者 +- [x] P1-6 发送队列上限:`ws_send_queue_`/代理队列设最大值,超限断开慢消费者 → ✅ 已实现(`setMaxWsQueueSize()`,默认 1000,入队前检查超限即关闭) - [ ] P2-1 CORS 白名单:origin 白名单校验,替代反射任意 Origin ### 中期 diff --git a/libhttp/README.md b/libhttp/README.md index 27ff679..b604398 100644 --- a/libhttp/README.md +++ b/libhttp/README.md @@ -15,6 +15,7 @@ C++20 network library for the UniServe microservice framework, providing unified - **Client Management**: WebSocket client tracking, broadcast, and targeted send - **WS Message Size Limit**: `setMaxWsMessageSize()` caps single WebSocket message (default 16MB, aligned with Beast; applies to server and proxy streams) to prevent OOM - **Read Timeouts (Slowloris Protection)**: `setHeaderTimeout()` (default 10s) / `setReadTimeout()` (default 30s) enforce two-stage header/body read timeouts plus WS idle timeout, aligned with WAF +- **WS Queue Size Limit**: `setMaxWsQueueSize()` (default 1000) caps WS send / proxy queues; slow consumers get disconnected to prevent unbounded-memory OOM ### HttpClient - **HTTP Client**: Synchronous HTTP requests via libcurl (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS) @@ -179,6 +180,10 @@ std::chrono::seconds getHeaderTimeout() const; void setReadTimeout(std::chrono::seconds t); std::chrono::seconds getReadTimeout() const; +// WS 发送/代理队列上限(条数)。默认 1000;超限判定慢消费者并关闭连接(防 OOM)。 +void setMaxWsQueueSize(std::size_t n); +std::size_t getMaxWsQueueSize() const; + **WebSocket Routes** ```cpp void ws(const std::string& path, WebSocketMessageHandler handler); @@ -295,6 +300,7 @@ using WebSocketClientBase = HttpClient; ## Version History +- **v3.4.3**: WebSocket 发送/代理队列上限(P1-6)。新增 `setMaxWsQueueSize()`(默认 1000);`wsSend` / `onProxyReadClient` / `onProxyReadBackend` 入队前检查,超限判定慢消费者并关闭连接,防止无界队列内存耗尽。 - **v3.4.2**: 服务器读超时防 Slowloris(P1-4)。新增 `setHeaderTimeout()`(默认 10s)/`setReadTimeout()`(默认 30s);`doRead` 两阶段读(`async_read_header` 头超时 → `async_read` 体超时),`doSSLHandshake` 覆盖握手超时,`doWsRead` 设 WS 空闲超时,对齐 WAF ReadHeaderTimeout/ReadTimeout。 - **v3.4.1**: WebSocket 单条消息大小限制(P1-3)。新增 `setMaxWsMessageSize()`/`getMaxWsMessageSize()`(默认 16MB,与 Beast 默认一致),服务端 `doWsUpgrade`/`doWssUpgrade` 与代理 `onProxyConnect`/`onProxySslHandshake` 创建流后调用 `read_message_max()`,超限消息由 Beast 在分配内存前拒绝并关闭连接,防 OOM。 - **v3.4.0**: HttpClient 响应大小限制(防 OOM)。`request_stream_sync` 改为真流式(按块回调,不二次缓冲);新增 `set_max_response_size()`/`get_max_response_size()`(默认 10MB,对齐 WAF,0 表示不限制),超限时 `write_callback` 返回 0 中止传输并抛异常。 diff --git a/libhttp/inc/us/HttpServer.hpp b/libhttp/inc/us/HttpServer.hpp index 1a51382..c31ff63 100644 --- a/libhttp/inc/us/HttpServer.hpp +++ b/libhttp/inc/us/HttpServer.hpp @@ -132,6 +132,11 @@ public: void setReadTimeout(std::chrono::seconds t) { read_timeout_ = t; } std::chrono::seconds getReadTimeout() const { return read_timeout_; } + // WS 发送/代理队列上限(条数)。默认 1000。防止慢消费者导致无界队列内存耗尽(P1-6)。 + // 超限时判定为慢消费者,关闭对应连接。 + void setMaxWsQueueSize(std::size_t n) { max_ws_queue_size_ = n; } + std::size_t getMaxWsQueueSize() const { return max_ws_queue_size_; } + class debug { public: static void print_all_header_fields(const http::request& req); @@ -150,6 +155,7 @@ private: std::size_t max_ws_message_size_ = 16 * 1024 * 1024; // 默认 16MB,与 Beast 默认一致 std::chrono::seconds header_timeout_{10}; // 头/握手超时,防 Slowloris(对齐 WAF ReadHeaderTimeout) std::chrono::seconds read_timeout_{30}; // 体超时(对齐 WAF ReadTimeout) + std::size_t max_ws_queue_size_ = 1000; // WS 发送/代理队列上限,防慢消费者 OOM(P1-6) bool ssl_enabled_{false}; std::string cert_file_; diff --git a/libhttp/src/HttpServer_WsSession.cpp b/libhttp/src/HttpServer_WsSession.cpp index 7673ffa..0b1016c 100644 --- a/libhttp/src/HttpServer_WsSession.cpp +++ b/libhttp/src/HttpServer_WsSession.cpp @@ -8,6 +8,14 @@ namespace net { void HttpServer::Session::wsSend(const std::string& message) { auto exec = stream_.get_executor(); asio::post(exec, [self = shared_from_this(), message]() { + // P1-6:超过队列上限,判定慢消费者,关闭连接防内存耗尽 + if (self->ws_send_queue_.size() >= self->server_->getMaxWsQueueSize()) { + std::cerr << "[WS] ws_send_queue_ overflow (" << self->ws_send_queue_.size() + << " >= " << self->server_->getMaxWsQueueSize() + << ") from " << self->getRemoteAddress() << ", closing" << std::endl; + self->close(); + return; + } self->ws_send_queue_.push_back(message); if (self->ws_send_queue_.size() == 1) { self->doWsWrite(); @@ -373,6 +381,13 @@ void HttpServer::Session::onProxyReadClient(beast::error_code ec, std::size_t by // Queue to backend auto exec = stream_.get_executor(); asio::post(exec, [self = shared_from_this(), data = std::move(data), is_binary]() { + // P1-6:超过队列上限,判定慢后端,关闭代理连接防内存耗尽 + if (self->proxy_to_backend_queue_.size() >= self->server_->getMaxWsQueueSize()) { + std::cerr << "[WS-Proxy] proxy_to_backend_queue_ overflow (" + << self->proxy_to_backend_queue_.size() << ") closing" << std::endl; + self->closeProxy(websocket::close_code::internal_error); + return; + } self->proxy_to_backend_queue_.emplace_back(std::move(data), is_binary); if (self->proxy_to_backend_queue_.size() == 1) { self->doProxyWriteBackend(); @@ -449,6 +464,13 @@ void HttpServer::Session::onProxyReadBackend(beast::error_code ec, std::size_t b // Queue to client auto exec = stream_.get_executor(); asio::post(exec, [self = shared_from_this(), data = std::move(data), is_binary]() { + // P1-6:超过队列上限,判定慢客户端,关闭代理连接防内存耗尽 + if (self->proxy_to_client_queue_.size() >= self->server_->getMaxWsQueueSize()) { + std::cerr << "[WS-Proxy] proxy_to_client_queue_ overflow (" + << self->proxy_to_client_queue_.size() << ") closing" << std::endl; + self->closeProxy(websocket::close_code::internal_error); + return; + } self->proxy_to_client_queue_.emplace_back(std::move(data), is_binary); if (self->proxy_to_client_queue_.size() == 1) { self->doProxyWriteClient();