Skip to content

Commit 35bc40b

Browse files
authored
Security hardening and test coverage improvements (#14)
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 35bc40b

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)