fix(libhttp): 服务器读超时防 Slowloris (P1-4)
- 新增 setHeaderTimeout()(default 10s) / setReadTimeout()(default 30s),对齐 WAF - doSSLHandshake 设置头超时覆盖握手,doRead 拆分为 async_read_header (头超时) + async_read (体超时) - doWsRead 每次读取设置空闲超时,自动清理长期空闲连接 - 更新审计文档 P1-4 状态、README v3.4.2
This commit is contained in:
1 parent
507054c4b2
commit
37c6d401d6
6 files changed
+64
-4
No files matched your search
@@ -11,9 +11,9 @@
|
|||||||
| 优先级 | 总数 | 已修复 | 部分修复 | 未修复 |
|
| 优先级 | 总数 | 已修复 | 部分修复 | 未修复 |
|
||||||
|--------|------|--------|----------|--------|
|
|--------|------|--------|----------|--------|
|
||||||
| P0 | 0 | 0 | 0 | 0 |
|
| P0 | 0 | 0 | 0 | 0 |
|
||||||
| P1 | 6 | 4 | 0 | 2 |
|
| P1 | 6 | 5 | 0 | 1 |
|
||||||
| P2 | 4 | 0 | 0 | 4 |
|
| P2 | 4 | 0 | 0 | 4 |
|
||||||
| **合计** | **10** | **6** | **0** | **4** |
|
| **合计** | **10** | **7** | **0** | **3** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,7 +24,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-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-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 消息无大小限制 | ✅ 已修复 | `HttpServer` 新增 `max_ws_message_size_`(默认 16MB,与 Beast 默认一致)与 `setMaxWsMessageSize()`/`getMaxWsMessageSize()`。`doWsUpgrade`/`doWssUpgrade` 及代理流 `onProxyConnect`/`onProxySslHandshake` 创建后调用 `read_message_max()`,超限消息由 Beast 在分配内存前拒绝并关闭连接,防 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-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-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) | ❌ 未修复 | `ws_send_queue_`、`proxy_to_backend_queue_`、`proxy_to_client_queue_`(`HttpServer_WsSession.cpp` L8-16/L362-367/L438-443)均为无界 `std::deque`。若客户端消费慢,服务端持续入队可导致内存耗尽 |
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
### 立即(24小时)
|
### 立即(24小时)
|
||||||
- [x] P1-1 请求体大小限制:`doRead` 改用 `http::request_parser` + `body_limit()`,`HttpServer` 增加 `setMaxBodySize()`(对齐 WAF 10MB),超限返回 413 而非断开 → ✅ 已实现(默认 10MB,`setMaxBodySize()` 可覆盖)
|
- [x] P1-1 请求体大小限制:`doRead` 改用 `http::request_parser` + `body_limit()`,`HttpServer` 增加 `setMaxBodySize()`(对齐 WAF 10MB),超限返回 413 而非断开 → ✅ 已实现(默认 10MB,`setMaxBodySize()` 可覆盖)
|
||||||
- [x] P1-2 HttpClient 响应上限:`write_callback` 累计超过上限即失败,返回错误 → ✅ 已实现(`WriteCtx` 流式回调 + 上限,默认 10MB,`set_max_response_size()` 可覆盖)
|
- [x] P1-2 HttpClient 响应上限:`write_callback` 累计超过上限即失败,返回错误 → ✅ 已实现(`WriteCtx` 流式回调 + 上限,默认 10MB,`set_max_response_size()` 可覆盖)
|
||||||
- [ ] P1-4 服务器读超时:入站 `stream_` 设置 `expires_after`(首字节/头+体超时),防 Slowloris
|
- [x] P1-4 服务器读超时:入站 `stream_` 设置 `expires_after`(首字节/头+体超时),防 Slowloris → ✅ 已实现(`setHeaderTimeout()` 默认 10s / `setReadTimeout()` 默认 30s,两阶段读 + WS 空闲超时)
|
||||||
|
|
||||||
### 短期(72小时)
|
### 短期(72小时)
|
||||||
- [x] P1-3 WS 消息大小限制:设置 `read_message_max`(可配置)→ ✅ 已实现(`setMaxWsMessageSize()`,默认 16MB,服务端与代理流均生效)
|
- [x] P1-3 WS 消息大小限制:设置 `read_message_max`(可配置)→ ✅ 已实现(`setMaxWsMessageSize()`,默认 16MB,服务端与代理流均生效)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ C++20 network library for the UniServe microservice framework, providing unified
|
|||||||
- **Thread Pool**: Multi-threaded I/O with `asio::strand` for thread safety
|
- **Thread Pool**: Multi-threaded I/O with `asio::strand` for thread safety
|
||||||
- **Client Management**: WebSocket client tracking, broadcast, and targeted send
|
- **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
|
- **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
|
||||||
|
|
||||||
### HttpClient
|
### HttpClient
|
||||||
- **HTTP Client**: Synchronous HTTP requests via libcurl (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)
|
- **HTTP Client**: Synchronous HTTP requests via libcurl (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)
|
||||||
@@ -172,6 +173,12 @@ void setStaticDirectory(const std::string& path);
|
|||||||
void setMaxWsMessageSize(std::size_t bytes);
|
void setMaxWsMessageSize(std::size_t bytes);
|
||||||
std::size_t getMaxWsMessageSize() const;
|
std::size_t getMaxWsMessageSize() const;
|
||||||
|
|
||||||
|
// 入站读超时(防 Slowloris)。默认 10s(头)/ 30s(体),对齐 WAF。
|
||||||
|
void setHeaderTimeout(std::chrono::seconds t);
|
||||||
|
std::chrono::seconds getHeaderTimeout() const;
|
||||||
|
void setReadTimeout(std::chrono::seconds t);
|
||||||
|
std::chrono::seconds getReadTimeout() const;
|
||||||
|
|
||||||
**WebSocket Routes**
|
**WebSocket Routes**
|
||||||
```cpp
|
```cpp
|
||||||
void ws(const std::string& path, WebSocketMessageHandler handler);
|
void ws(const std::string& path, WebSocketMessageHandler handler);
|
||||||
@@ -288,6 +295,7 @@ using WebSocketClientBase = HttpClient;
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
|
- **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.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.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.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.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace pmc {
|
namespace pmc {
|
||||||
namespace net {
|
namespace net {
|
||||||
@@ -124,6 +125,13 @@ public:
|
|||||||
void setMaxWsMessageSize(std::size_t bytes) { max_ws_message_size_ = bytes; }
|
void setMaxWsMessageSize(std::size_t bytes) { max_ws_message_size_ = bytes; }
|
||||||
std::size_t getMaxWsMessageSize() const { return max_ws_message_size_; }
|
std::size_t getMaxWsMessageSize() const { return max_ws_message_size_; }
|
||||||
|
|
||||||
|
// 入站读超时(防 Slowloris,P1-4)。默认与 WAF 对齐:头 10s、体 30s。
|
||||||
|
// 头超时覆盖 TCP 连接建立 + SSL 握手 + 请求头读取;体超时覆盖请求体读取。
|
||||||
|
void setHeaderTimeout(std::chrono::seconds t) { header_timeout_ = t; }
|
||||||
|
std::chrono::seconds getHeaderTimeout() const { return header_timeout_; }
|
||||||
|
void setReadTimeout(std::chrono::seconds t) { read_timeout_ = t; }
|
||||||
|
std::chrono::seconds getReadTimeout() const { return read_timeout_; }
|
||||||
|
|
||||||
class debug {
|
class debug {
|
||||||
public:
|
public:
|
||||||
static void print_all_header_fields(const http::request<http::string_body>& req);
|
static void print_all_header_fields(const http::request<http::string_body>& req);
|
||||||
@@ -140,6 +148,8 @@ private:
|
|||||||
|
|
||||||
std::size_t max_body_size_ = 10 * 1024 * 1024; // 默认 10MB,与 WAF 对齐
|
std::size_t max_body_size_ = 10 * 1024 * 1024; // 默认 10MB,与 WAF 对齐
|
||||||
std::size_t max_ws_message_size_ = 16 * 1024 * 1024; // 默认 16MB,与 Beast 默认一致
|
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)
|
||||||
|
|
||||||
bool ssl_enabled_{false};
|
bool ssl_enabled_{false};
|
||||||
std::string cert_file_;
|
std::string cert_file_;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ private:
|
|||||||
void close();
|
void close();
|
||||||
void doSSLHandshake();
|
void doSSLHandshake();
|
||||||
void doRead();
|
void doRead();
|
||||||
|
void doReadBody();
|
||||||
void handleRead(beast::error_code ec);
|
void handleRead(beast::error_code ec);
|
||||||
bool isWebSocketUpgrade(const http::request<http::string_body>& req);
|
bool isWebSocketUpgrade(const http::request<http::string_body>& req);
|
||||||
void doWsUpgrade();
|
void doWsUpgrade();
|
||||||
|
|||||||
@@ -167,6 +167,13 @@ void HttpServer::Session::doSSLHandshake() {
|
|||||||
std::cout << "[SSL] Starting handshake for "
|
std::cout << "[SSL] Starting handshake for "
|
||||||
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
<< remote_ip_ << ":" << remote_port_ << std::endl;
|
||||||
|
|
||||||
|
// P1-4:设置头超时(TCP 连接完成后,覆盖 SSL 握手 + 请求头读取)
|
||||||
|
if (ssl_enabled_ && ssl_stream_) {
|
||||||
|
ssl_stream_->next_layer().expires_after(server_->getHeaderTimeout());
|
||||||
|
} else {
|
||||||
|
stream_.expires_after(server_->getHeaderTimeout());
|
||||||
|
}
|
||||||
|
|
||||||
ssl_stream_->async_handshake(
|
ssl_stream_->async_handshake(
|
||||||
asio::ssl::stream_base::server,
|
asio::ssl::stream_base::server,
|
||||||
[self](beast::error_code ec) {
|
[self](beast::error_code ec) {
|
||||||
@@ -193,6 +200,37 @@ void HttpServer::Session::doRead() {
|
|||||||
parser_.emplace();
|
parser_.emplace();
|
||||||
parser_->body_limit(server_->max_body_size_);
|
parser_->body_limit(server_->max_body_size_);
|
||||||
|
|
||||||
|
// P1-4:阶段1 读请求头(头超时,防 Slowloris)
|
||||||
|
if (ssl_enabled_ && ssl_stream_) {
|
||||||
|
ssl_stream_->next_layer().expires_after(server_->getHeaderTimeout());
|
||||||
|
} else {
|
||||||
|
stream_.expires_after(server_->getHeaderTimeout());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto on_header = [self](beast::error_code ec, std::size_t) {
|
||||||
|
if (ec) {
|
||||||
|
self->handleRead(ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 阶段2 读请求体:切换到体超时
|
||||||
|
if (self->ssl_enabled_ && self->ssl_stream_) {
|
||||||
|
self->ssl_stream_->next_layer().expires_after(self->server_->getReadTimeout());
|
||||||
|
} else {
|
||||||
|
self->stream_.expires_after(self->server_->getReadTimeout());
|
||||||
|
}
|
||||||
|
self->doReadBody();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ssl_enabled_ && ssl_stream_) {
|
||||||
|
http::async_read_header(*ssl_stream_, buffer_, *parser_, on_header);
|
||||||
|
} else {
|
||||||
|
http::async_read_header(stream_, buffer_, *parser_, on_header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpServer::Session::doReadBody() {
|
||||||
|
auto self = shared_from_this();
|
||||||
|
|
||||||
auto on_read = [self](beast::error_code ec, std::size_t) {
|
auto on_read = [self](beast::error_code ec, std::size_t) {
|
||||||
if (!ec) self->request_ = self->parser_->release();
|
if (!ec) self->request_ = self->parser_->release();
|
||||||
self->handleRead(ec);
|
self->handleRead(ec);
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ void HttpServer::Session::doWsRead() {
|
|||||||
auto self = shared_from_this();
|
auto self = shared_from_this();
|
||||||
ws_buffer_.consume(ws_buffer_.size());
|
ws_buffer_.consume(ws_buffer_.size());
|
||||||
|
|
||||||
|
// P1-4:WS 读取设置空闲超时,清理长期不发消息的连接
|
||||||
|
stream_.expires_after(server_->getReadTimeout());
|
||||||
|
|
||||||
if (ssl_enabled_ && wss_stream_) {
|
if (ssl_enabled_ && wss_stream_) {
|
||||||
wss_stream_->async_read(
|
wss_stream_->async_read(
|
||||||
ws_buffer_,
|
ws_buffer_,
|
||||||
|
|||||||
Reference in new issue
Block a user