Skip to content

Commit f4b7b0d

Browse files
committed
Security hardening and test coverage improvements
This commit addresses multiple security vulnerabilities and adds comprehensive test coverage for HTTP client API integration. Security Fixes: - Add payload and timeout limits to HTTP/SSE servers (issue #3) - Fix SSE session security with crypto-random IDs and session binding (issue #2) - Add optional authentication and restrict CORS (issue #1) - Fix HTTP client scheme handling and disable redirects (issue #4) - Add security middleware for logging, rate limiting, and concurrency control (issue #5) Test Coverage: - Add HTTP client API integration tests (not LoopbackTransport) - Add SSE HTTP integration tests with real network stack - Fix SSE server test to extract and use session_id All 45 tests passing (100% pass rate)
1 parent b2b2772 commit f4b7b0d

17 files changed

Lines changed: 1956 additions & 44 deletions

CMakeLists.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_library(fastmcpp_core
2525
src/server/server.cpp
2626
src/server/context.cpp
2727
src/server/middleware.cpp
28+
src/server/security_middleware.cpp
2829
src/server/http_server.cpp
2930
src/server/stdio_server.cpp
3031
src/server/sse_server.cpp
@@ -250,10 +251,40 @@ if(FASTMCPP_BUILD_TESTS)
250251
target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core)
251252
add_test(NAME fastmcpp_server_context_meta COMMAND fastmcpp_server_context_meta)
252253

254+
add_executable(fastmcpp_server_security_limits tests/server/security_limits.cpp)
255+
target_link_libraries(fastmcpp_server_security_limits PRIVATE fastmcpp_core)
256+
add_test(NAME fastmcpp_server_security_limits COMMAND fastmcpp_server_security_limits)
257+
258+
add_executable(fastmcpp_server_sse_session_security tests/server/sse_session_security.cpp)
259+
target_link_libraries(fastmcpp_server_sse_session_security PRIVATE fastmcpp_core)
260+
add_test(NAME fastmcpp_server_sse_session_security COMMAND fastmcpp_server_sse_session_security)
261+
262+
# SSE session security with fastmcpp::client::HttpTransport (not raw httplib)
263+
add_executable(fastmcpp_client_sse_session_client tests/client/sse_session_client.cpp)
264+
target_link_libraries(fastmcpp_client_sse_session_client PRIVATE fastmcpp_core)
265+
add_test(NAME fastmcpp_client_sse_session_client COMMAND fastmcpp_client_sse_session_client)
266+
267+
# SSE + HTTP integration (real network, not LoopbackTransport)
268+
add_executable(fastmcpp_server_sse_http_integration tests/server/sse_http_integration.cpp)
269+
target_link_libraries(fastmcpp_server_sse_http_integration PRIVATE fastmcpp_core)
270+
add_test(NAME fastmcpp_server_sse_http_integration COMMAND fastmcpp_server_sse_http_integration)
271+
272+
add_executable(fastmcpp_server_auth_cors_security tests/server/auth_cors_security.cpp)
273+
target_link_libraries(fastmcpp_server_auth_cors_security PRIVATE fastmcpp_core)
274+
add_test(NAME fastmcpp_server_auth_cors_security COMMAND fastmcpp_server_auth_cors_security)
275+
276+
add_executable(fastmcpp_server_security_middleware tests/server/security_middleware.cpp)
277+
target_link_libraries(fastmcpp_server_security_middleware PRIVATE fastmcpp_core)
278+
add_test(NAME fastmcpp_server_security_middleware COMMAND fastmcpp_server_security_middleware)
279+
253280
add_executable(fastmcpp_client_transports tests/client/transports.cpp)
254281
target_link_libraries(fastmcpp_client_transports PRIVATE fastmcpp_core)
255282
add_test(NAME fastmcpp_client_transports COMMAND fastmcpp_client_transports)
256283

284+
add_executable(fastmcpp_client_http_client_security tests/client/http_client_security.cpp)
285+
target_link_libraries(fastmcpp_client_http_client_security PRIVATE fastmcpp_core)
286+
add_test(NAME fastmcpp_client_http_client_security COMMAND fastmcpp_client_http_client_security)
287+
257288
add_executable(fastmcpp_client_api_basic tests/client/api_basic.cpp)
258289
target_link_libraries(fastmcpp_client_api_basic PRIVATE fastmcpp_core)
259290
add_test(NAME fastmcpp_client_api_basic COMMAND fastmcpp_client_api_basic)

examples/streaming_demo.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ int main()
4242
std::vector<int> seen;
4343
std::mutex m;
4444
std::atomic<bool> sse_connected{false};
45+
std::string session_id;
4546

4647
httplib::Client cli("127.0.0.1", port);
4748
cli.set_connection_timeout(std::chrono::seconds(10));
@@ -53,6 +54,28 @@ int main()
5354
{
5455
sse_connected = true;
5556
std::string chunk(data, len);
57+
58+
// Parse SSE endpoint event to extract session_id
59+
if (chunk.find("event: endpoint") != std::string::npos)
60+
{
61+
size_t data_pos = chunk.find("data: ");
62+
if (data_pos != std::string::npos)
63+
{
64+
size_t start = data_pos + 6;
65+
size_t end = chunk.find_first_of("\n\r", start);
66+
std::string endpoint_url = chunk.substr(start, end - start);
67+
68+
size_t sid_pos = endpoint_url.find("session_id=");
69+
if (sid_pos != std::string::npos)
70+
{
71+
size_t sid_start = sid_pos + 11;
72+
size_t sid_end = endpoint_url.find_first_of("&\n\r", sid_start);
73+
std::lock_guard<std::mutex> lock(m);
74+
session_id = endpoint_url.substr(sid_start, sid_end - sid_start);
75+
}
76+
}
77+
}
78+
5679
if (chunk.find("data: ") == 0)
5780
{
5881
size_t start = 6;
@@ -102,11 +125,36 @@ int main()
102125
return 1;
103126
}
104127

128+
// Wait for session_id to be extracted
129+
for (int i = 0; i < 100; ++i)
130+
{
131+
std::lock_guard<std::mutex> lock(m);
132+
if (!session_id.empty())
133+
break;
134+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
135+
}
136+
137+
std::string sid;
138+
{
139+
std::lock_guard<std::mutex> lock(m);
140+
sid = session_id;
141+
}
142+
143+
if (sid.empty())
144+
{
145+
server->stop();
146+
if (sse_thread.joinable())
147+
sse_thread.join();
148+
std::cerr << "Failed to extract session_id" << std::endl;
149+
return 1;
150+
}
151+
105152
httplib::Client post("127.0.0.1", port);
106153
for (int i = 1; i <= 3; ++i)
107154
{
108155
Json j = Json{{"n", i}};
109-
auto res = post.Post("/messages", j.dump(), "application/json");
156+
std::string post_url = "/messages?session_id=" + sid;
157+
auto res = post.Post(post_url, j.dump(), "application/json");
110158
if (!res || res->status != 200)
111159
{
112160
server->stop();

include/fastmcpp/server/http_server.hpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,18 @@ namespace fastmcpp::server
1717
class HttpServerWrapper
1818
{
1919
public:
20+
/**
21+
* Construct an HTTP server with a core Server instance.
22+
*
23+
* @param core Shared pointer to the core Server (routes handler)
24+
* @param host Host address to bind to (default: "127.0.0.1" for localhost)
25+
* @param port Port to listen on (default: 18080)
26+
* @param auth_token Optional auth token for Bearer authentication (empty = no auth required)
27+
* @param cors_origin Optional CORS origin to allow (empty = no CORS header, use "*" for
28+
* wildcard)
29+
*/
2030
HttpServerWrapper(std::shared_ptr<Server> core, std::string host = "127.0.0.1",
21-
int port = 18080);
31+
int port = 18080, std::string auth_token = "", std::string cors_origin = "");
2232
~HttpServerWrapper();
2333

2434
bool start();
@@ -37,9 +47,13 @@ class HttpServerWrapper
3747
}
3848

3949
private:
50+
bool check_auth(const std::string& auth_header) const;
51+
4052
std::shared_ptr<Server> core_;
4153
std::string host_;
4254
int port_;
55+
std::string auth_token_; // Optional Bearer token for authentication
56+
std::string cors_origin_; // Optional CORS origin (empty = no CORS)
4357
std::unique_ptr<httplib::Server> svr_;
4458
std::thread thread_;
4559
std::atomic<bool> running_{false};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#pragma once
2+
#include "fastmcpp/server/middleware.hpp"
3+
#include "fastmcpp/types.hpp"
4+
5+
#include <atomic>
6+
#include <chrono>
7+
#include <deque>
8+
#include <functional>
9+
#include <mutex>
10+
#include <string>
11+
#include <unordered_map>
12+
13+
namespace fastmcpp::server
14+
{
15+
16+
/// Log entry for a request
17+
struct RequestLogEntry
18+
{
19+
std::chrono::system_clock::time_point timestamp;
20+
std::string route;
21+
size_t payload_size;
22+
bool success;
23+
std::string error_message; // Empty if success
24+
};
25+
26+
/// Logging callback function type
27+
using LogCallback = std::function<void(const RequestLogEntry&)>;
28+
29+
/// Logging middleware for audit trail (v2.13.0+)
30+
///
31+
/// Provides optional request logging to track all route/tool invocations.
32+
/// Can be used as both BeforeHook and AfterHook for comprehensive logging.
33+
///
34+
/// Usage:
35+
/// ```cpp
36+
/// auto logger = std::make_shared<LoggingMiddleware>(
37+
/// [](const RequestLogEntry& entry) {
38+
/// std::cout << entry.timestamp << " " << entry.route << std::endl;
39+
/// });
40+
/// srv.add_before(logger->create_before_hook());
41+
/// srv.add_after(logger->create_after_hook());
42+
/// ```
43+
class LoggingMiddleware
44+
{
45+
public:
46+
explicit LoggingMiddleware(LogCallback callback) : callback_(std::move(callback)) {}
47+
48+
/// Create a BeforeHook that logs incoming requests
49+
BeforeHook create_before_hook();
50+
51+
/// Create an AfterHook that logs completed requests
52+
AfterHook create_after_hook();
53+
54+
private:
55+
LogCallback callback_;
56+
std::mutex mutex_;
57+
std::unordered_map<std::string, size_t> request_sizes_; // Track sizes for after hook
58+
};
59+
60+
/// Rate limiting middleware for DoS prevention (v2.13.0+)
61+
///
62+
/// Enforces per-route request limits using a sliding window algorithm.
63+
/// Rejects requests that exceed the configured rate.
64+
///
65+
/// Usage:
66+
/// ```cpp
67+
/// auto limiter = std::make_shared<RateLimitMiddleware>(
68+
/// 100, // max requests
69+
/// std::chrono::minutes(1) // per time window
70+
/// );
71+
/// srv.add_before(limiter->create_hook());
72+
/// ```
73+
class RateLimitMiddleware
74+
{
75+
public:
76+
/// Construct rate limiter
77+
/// @param max_requests Maximum requests allowed in time window
78+
/// @param window Time window for rate limiting
79+
RateLimitMiddleware(size_t max_requests,
80+
std::chrono::steady_clock::duration window = std::chrono::minutes(1))
81+
: max_requests_(max_requests), window_(window)
82+
{
83+
}
84+
85+
/// Create a BeforeHook that enforces rate limits
86+
BeforeHook create_hook();
87+
88+
/// Get current request count for a route
89+
size_t get_request_count(const std::string& route);
90+
91+
/// Reset rate limit counters (for testing)
92+
void reset();
93+
94+
private:
95+
size_t max_requests_;
96+
std::chrono::steady_clock::duration window_;
97+
std::mutex mutex_;
98+
99+
struct RouteStats
100+
{
101+
std::deque<std::chrono::steady_clock::time_point> timestamps;
102+
};
103+
104+
std::unordered_map<std::string, RouteStats> stats_;
105+
106+
void cleanup_old_entries(RouteStats& stats);
107+
};
108+
109+
/// Concurrency limiting middleware for resource control (v2.13.0+)
110+
///
111+
/// Limits the number of concurrent route handler executions.
112+
/// Uses atomic counters for thread-safe tracking.
113+
///
114+
/// Usage:
115+
/// ```cpp
116+
/// auto limiter = std::make_shared<ConcurrencyLimitMiddleware>(10); // Max 10 parallel
117+
/// srv.add_before(limiter->create_before_hook());
118+
/// srv.add_after(limiter->create_after_hook());
119+
/// ```
120+
class ConcurrencyLimitMiddleware
121+
{
122+
public:
123+
/// Construct concurrency limiter
124+
/// @param max_concurrent Maximum number of concurrent handler executions
125+
explicit ConcurrencyLimitMiddleware(size_t max_concurrent) : max_concurrent_(max_concurrent) {}
126+
127+
/// Create a BeforeHook that checks concurrency limit
128+
BeforeHook create_before_hook();
129+
130+
/// Create an AfterHook that releases concurrency slot
131+
AfterHook create_after_hook();
132+
133+
/// Get current concurrent request count
134+
size_t get_current_count() const
135+
{
136+
return current_count_.load();
137+
}
138+
139+
private:
140+
size_t max_concurrent_;
141+
std::atomic<size_t> current_count_{0};
142+
};
143+
144+
} // namespace fastmcpp::server

include/fastmcpp/server/sse_server.hpp

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <queue>
1212
#include <string>
1313
#include <thread>
14+
#include <unordered_map>
1415

1516
namespace fastmcpp::server
1617
{
@@ -50,10 +51,13 @@ class SseServerWrapper
5051
* @param port Port to listen on (default: 18080)
5152
* @param sse_path Path for SSE GET endpoint (default: "/sse")
5253
* @param message_path Path for POST message endpoint (default: "/messages")
54+
* @param auth_token Optional auth token for Bearer authentication (empty = no auth required)
55+
* @param cors_origin Optional CORS origin to allow (empty = no CORS header, use "*" for
56+
* wildcard)
5357
*/
5458
explicit SseServerWrapper(McpHandler handler, std::string host = "127.0.0.1", int port = 18080,
55-
std::string sse_path = "/sse",
56-
std::string message_path = "/messages");
59+
std::string sse_path = "/sse", std::string message_path = "/messages",
60+
std::string auth_token = "", std::string cors_origin = "");
5761

5862
~SseServerWrapper();
5963

@@ -118,29 +122,40 @@ class SseServerWrapper
118122
private:
119123
void run_server();
120124
void send_event_to_all_clients(const fastmcpp::Json& event);
125+
void send_event_to_session(const std::string& session_id, const fastmcpp::Json& event);
126+
std::string generate_session_id();
127+
bool check_auth(const std::string& auth_header) const;
121128

122129
McpHandler handler_;
123130
std::string host_;
124131
int port_;
125132
std::string sse_path_;
126133
std::string message_path_;
134+
std::string auth_token_; // Optional Bearer token for authentication
135+
std::string cors_origin_; // Optional CORS origin (empty = no CORS)
127136

128137
std::unique_ptr<httplib::Server> svr_;
129138
std::thread thread_;
130139
std::atomic<bool> running_{false};
131140

141+
// Security limits
142+
static constexpr size_t MAX_CONNECTIONS = 100;
143+
static constexpr size_t MAX_QUEUE_SIZE = 1000;
144+
132145
struct ConnectionState
133146
{
147+
std::string session_id;
134148
std::deque<fastmcpp::Json> queue;
135149
std::mutex m;
136150
std::condition_variable cv;
137151
bool alive{true};
138152
};
139153

140-
void handle_sse_connection(httplib::DataSink& sink, std::shared_ptr<ConnectionState> conn);
154+
void handle_sse_connection(httplib::DataSink& sink, std::shared_ptr<ConnectionState> conn,
155+
const std::string& session_id);
141156

142-
// Active SSE connections (per-connection queues)
143-
std::vector<std::shared_ptr<ConnectionState>> connections_;
157+
// Active SSE connections mapped by session ID
158+
std::unordered_map<std::string, std::shared_ptr<ConnectionState>> connections_;
144159
std::mutex conns_mutex_;
145160
};
146161

0 commit comments

Comments
 (0)