fix(libhttp): WebSocket 单条消息大小限制防 OOM (P1-3)

- 新增 setMaxWsMessageSize/getMaxWsMessageSize(默认 16MB,与 Beast 默认一致)
- 服务端 doWsUpgrade/doWssUpgrade 与代理流 onProxyConnect/onProxySslHandshake 创建后调用 read_message_max
- 超限消息由 Beast 在分配内存前拒绝并关闭连接
- 更新审计文档 P1-3 状态、README v3.4.1
This commit is contained in:
qing committed 2026-08-04 16:03:06 +08:00
1 parent 85e0d0740b
commit 507054c4b2
4 files changed
+26 -4

No files matched your search

@@ -11,9 +11,9 @@
| 优先级 | 总数 | 已修复 | 部分修复 | 未修复 |
|--------|------|--------|----------|--------|
| P0 | 0 | 0 | 0 | 0 |
| P1 | 6 | 3 | 0 | 3 |
| P1 | 6 | 4 | 0 | 2 |
| P2 | 4 | 0 | 0 | 4 |
| **合计** | **10** | **5** | **0** | **5** |
| **合计** | **10** | **6** | **0** | **4** |
---
@@ -23,7 +23,7 @@
|---|------|------|------|
| P1-1 | 请求体大小限制不可配置 + 超限无 413 | ✅ 已修复 | `Session::doRead`(`HttpServer_Request.cpp` L167-186)改用 `http::request_parser` + `body_limit()`(默认 10MB,与 WAF 对齐);`HttpServer` 新增 `setMaxBodySize()`/`getMaxBodySize()`(`HttpServer.hpp` L118-120)。超限时 `handleRead` 返回 **413 Payload Too Large** 并关闭连接(L256-265),而非静默断连 |
| P1-2 | HttpClient 响应无大小限制 | ✅ 已修复 | `HttpClient.cpp` 新增 `WriteCtx`(流式回调 + 累计字节 + 上限),`write_callback` 超限返回 0 触发 `CURLE_WRITE_ERROR`,`request_stream_sync` 抛异常;`request_stream_sync` 改为**真流式**(callback 非空时每块即回调,不二次累积),`HttpClient` 新增 `set_max_response_size()`/`get_max_response_size()`(默认 10MB,对齐 WAF,0 表示不限制) |
| P1-3 | WebSocket 消息无大小限制 | ❌ 未修复 | `Session::onWsRead`(`HttpServer_WsSession.cpp` L88-111)把整条 WS 消息读入内存,依赖 Beast 默认 `read_message_max`(16MB)不可配置;超大消息可被恶意客户端推送导致 OOM |
| 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_`(`beast::tcp_stream`)**从未设置 `expires_after`**(仅代理后端设了超时)。攻击者建立连接后缓慢发送 HTTP 头,可无限占用连接与线程,Slowloris/连接耗尽(对应全局审计 P2-4) |
| 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`。若客户端消费慢,服务端持续入队可导致内存耗尽 |
@@ -72,7 +72,7 @@
- [ ] P1-4 服务器读超时:入站 `stream_` 设置 `expires_after`(首字节/头+体超时),防 Slowloris
### 短期(72小时)
- [ ] P1-3 WS 消息大小限制:设置 `read_message_max`(可配置)
- [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_`/代理队列设最大值,超限断开慢消费者
- [ ] P2-1 CORS 白名单:origin 白名单校验,替代反射任意 Origin
+6
View File
@@ -13,6 +13,7 @@ C++20 network library for the UniServe microservice framework, providing unified
- **Static Files**: `setStaticDirectory()` for serving static files
- **Thread Pool**: Multi-threaded I/O with `asio::strand` for thread safety
- **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
### HttpClient
- **HTTP Client**: Synchronous HTTP requests via libcurl (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)
@@ -167,6 +168,10 @@ void use(MiddlewareHandler middleware);
void setStaticDirectory(const std::string& path);
```
// WebSocket 单条消息大小上限(字节)。默认 16MB,与 Beast 默认一致;超限由 Beast 拒绝并关闭连接(防 OOM)。
void setMaxWsMessageSize(std::size_t bytes);
std::size_t getMaxWsMessageSize() const;
**WebSocket Routes**
```cpp
void ws(const std::string& path, WebSocketMessageHandler handler);
@@ -283,6 +288,7 @@ using WebSocketClientBase = HttpClient;
## Version History
- **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 中止传输并抛异常。
- **v3.3.0**: Split HttpServer into 6 modular translation units by functional area (core/lifecycle, listener, routes, request dispatch, WS management, WS session/proxy) for dramatically faster incremental builds. Added internal detail header `inc/us/detail/HttpServerImpl.hpp` for shared definitions across TU boundaries.
- **v3.2.0**: Split into two shared libraries — `libus_http.so` (server only, no curl) and `libus_httpclient.so` (HttpClient, depends on curl). HttpClient implementation moved to `src/HttpClient.cpp`; `HttpClient.hpp` is declaration-only.
+6
View File
@@ -119,6 +119,11 @@ public:
void setMaxBodySize(std::size_t bytes) { max_body_size_ = bytes; }
std::size_t getMaxBodySize() const { return max_body_size_; }
// WebSocket 单条消息大小上限(字节)。默认 16MB,与 Beast 默认一致(防 OOM)。
// 超限时 Beast 拒绝该消息并关闭连接(P1-3)。
void setMaxWsMessageSize(std::size_t bytes) { max_ws_message_size_ = bytes; }
std::size_t getMaxWsMessageSize() const { return max_ws_message_size_; }
class debug {
public:
static void print_all_header_fields(const http::request<http::string_body>& req);
@@ -134,6 +139,7 @@ private:
std::atomic<bool> running_{false};
std::size_t max_body_size_ = 10 * 1024 * 1024; // 默认 10MB,与 WAF 对齐
std::size_t max_ws_message_size_ = 16 * 1024 * 1024; // 默认 16MB,与 Beast 默认一致
bool ssl_enabled_{false};
std::string cert_file_;
+10
View File
@@ -19,6 +19,8 @@ void HttpServer::Session::doWsUpgrade() {
auto self = shared_from_this();
ws_stream_ = std::make_unique<ws_stream>(stream_);
ws_stream_->text(true);
// P1-3:限制单条 WS 消息大小,超限由 Beast 拒绝并关闭连接(防 OOM)
ws_stream_->read_message_max(server_->getMaxWsMessageSize());
ws_stream_->async_accept(
request_,
@@ -31,6 +33,8 @@ void HttpServer::Session::doWssUpgrade() {
auto self = shared_from_this();
wss_stream_ = std::make_unique<wss_stream>(*ssl_stream_);
wss_stream_->text(true);
// P1-3:限制单条 WS 消息大小,超限由 Beast 拒绝并关闭连接(防 OOM)
wss_stream_->read_message_max(server_->getMaxWsMessageSize());
wss_stream_->async_accept(
request_,
@@ -261,6 +265,9 @@ void HttpServer::Session::onProxyConnect(beast::error_code ec, tcp::endpoint /*e
proxy_ws_stream_ = std::make_unique<proxy_ws_stream>(proxy_tcp_stream_->socket());
proxy_tcp_stream_->expires_never();
// P1-3:限制代理方向单条 WS 消息大小(防 OOM)
proxy_ws_stream_->read_message_max(server_->getMaxWsMessageSize());
proxy_ws_stream_->set_option(websocket::stream_base::decorator(
[](websocket::request_type& req) {
req.set(http::field::user_agent, "UniServe-WsProxy");
@@ -286,6 +293,9 @@ void HttpServer::Session::onProxySslHandshake(beast::error_code ec) {
proxy_wss_stream_ = std::make_unique<proxy_wss_stream>(*proxy_ssl_stream_);
proxy_tcp_stream_->expires_never();
// P1-3:限制代理方向单条 WS 消息大小(防 OOM)
proxy_wss_stream_->read_message_max(server_->getMaxWsMessageSize());
proxy_wss_stream_->set_option(websocket::stream_base::decorator(
[](websocket::request_type& req) {
req.set(http::field::user_agent, "UniServe-WsProxy");