Skip to content

Commit b0367c8

Browse files
committed
fix: address CORS code review feedback
- Fix CORS enabled logic to check only allowed_origins (not methods/headers) - Add security validation: reject credentials=true with wildcard origin - Move Access-Control-Max-Age header to preflight responses only - Add PUT to default allowed_methods for REST API compatibility - Fix misleading YAML comment about empty origins behavior - Optimize: pre-build CORS header strings in constructor - Add 7 CORS integration tests covering all scenarios
1 parent 58f5b59 commit b0367c8

File tree

6 files changed

+325
-32
lines changed

6 files changed

+325
-32
lines changed

src/ros2_medkit_gateway/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ if(BUILD_TESTING)
9696
TIMEOUT 120
9797
)
9898

99+
# Add CORS integration tests
100+
add_launch_test(
101+
test/test_cors.test.py
102+
TARGET test_cors
103+
TIMEOUT 60
104+
)
105+
99106
# Demo automotive nodes
100107
add_executable(demo_engine_temp_sensor
101108
test/demo_nodes/engine_temp_sensor.cpp

src/ros2_medkit_gateway/config/gateway_params.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ ros2_medkit_gateway:
3232
# List of allowed origins
3333
# Examples: ["http://localhost:5173", "https://example.com"]
3434
# Use ["*"] to allow all origins (not recommended for production)
35-
# Empty list with other settings = allow all origins
35+
# Empty list = allow all origins (not recommended for production)
3636
allowed_origins: []
3737

3838
# Allowed HTTP methods for CORS requests
39-
# Default: ["GET", "OPTIONS"]
40-
allowed_methods: ["GET", "OPTIONS"]
39+
# Default: ["GET", "PUT", "OPTIONS"]
40+
allowed_methods: ["GET", "PUT", "OPTIONS"]
4141

4242
# Allowed headers in CORS requests
4343
# Use ["*"] to allow all headers

src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class RESTServer {
5959
std::string host_;
6060
int port_;
6161
CorsConfig cors_config_;
62+
63+
// Pre-built CORS header values (built once in constructor for performance)
64+
std::string cors_methods_header_;
65+
std::string cors_headers_header_;
66+
6267
std::unique_ptr<httplib::Server> server_;
6368
};
6469

src/ros2_medkit_gateway/src/gateway_node.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ GatewayNode::GatewayNode() : Node("ros2_medkit_gateway") {
2828
declare_parameter("server.port", 8080);
2929
declare_parameter("refresh_interval_ms", 2000);
3030
declare_parameter("cors.allowed_origins", std::vector<std::string>{});
31-
declare_parameter("cors.allowed_methods", std::vector<std::string>{"GET", "OPTIONS"});
31+
declare_parameter("cors.allowed_methods", std::vector<std::string>{"GET", "PUT", "OPTIONS"});
3232
declare_parameter("cors.allowed_headers", std::vector<std::string>{"Content-Type", "Accept"});
3333
declare_parameter("cors.allow_credentials", false);
3434
declare_parameter("cors.max_age_seconds", 86400);
@@ -45,10 +45,19 @@ GatewayNode::GatewayNode() : Node("ros2_medkit_gateway") {
4545
cors_config_.allow_credentials = get_parameter("cors.allow_credentials").as_bool();
4646
cors_config_.max_age_seconds = get_parameter("cors.max_age_seconds").as_int();
4747

48-
// CORS is enabled if any origins are configured or if methods/headers are non-default
49-
// Treat missing/empty config as disabled for security
50-
cors_config_.enabled = !cors_config_.allowed_origins.empty() || !cors_config_.allowed_methods.empty() ||
51-
!cors_config_.allowed_headers.empty();
48+
// CORS is enabled only if allowed_origins is configured
49+
// Treat missing/empty origins as disabled for security
50+
cors_config_.enabled = !cors_config_.allowed_origins.empty();
51+
52+
// Validate CORS configuration: credentials cannot be used with wildcard origin
53+
if (cors_config_.enabled && cors_config_.allow_credentials) {
54+
for (const auto & origin : cors_config_.allowed_origins) {
55+
if (origin == "*") {
56+
RCLCPP_ERROR(get_logger(), "CORS: allow_credentials cannot be true when allowed_origins contains '*'");
57+
throw std::runtime_error("Invalid CORS configuration");
58+
}
59+
}
60+
}
5261

5362
// Validate port range
5463
if (server_port_ < 1024 || server_port_ > 65535) {

src/ros2_medkit_gateway/src/rest_server.cpp

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ RESTServer::RESTServer(GatewayNode * node, const std::string & host, int port, c
3131
: node_(node), host_(host), port_(port), cors_config_(cors_config) {
3232
server_ = std::make_unique<httplib::Server>();
3333

34+
// Pre-build CORS header strings once (performance optimization)
35+
for (const auto & method : cors_config_.allowed_methods) {
36+
if (!cors_methods_header_.empty()) {
37+
cors_methods_header_ += ", ";
38+
}
39+
cors_methods_header_ += method;
40+
}
41+
for (const auto & header : cors_config_.allowed_headers) {
42+
if (!cors_headers_header_.empty()) {
43+
cors_headers_header_ += ", ";
44+
}
45+
cors_headers_header_ += header;
46+
}
47+
3448
// Set up pre-routing handler for CORS (only if enabled)
3549
if (cors_config_.enabled) {
3650
server_->set_pre_routing_handler([this](const httplib::Request & req, httplib::Response & res) {
@@ -41,6 +55,8 @@ RESTServer::RESTServer(GatewayNode * node, const std::string & host, int port, c
4155

4256
// Handle preflight OPTIONS requests
4357
if (req.method == "OPTIONS") {
58+
// Set Max-Age only for preflight responses
59+
res.set_header("Access-Control-Max-Age", std::to_string(cors_config_.max_age_seconds));
4460
res.status = 204; // No Content
4561
return httplib::Server::HandlerResponse::Handled;
4662
}
@@ -586,37 +602,18 @@ void RESTServer::handle_component_topic_publish(const httplib::Request & req, ht
586602
void RESTServer::set_cors_headers(httplib::Response & res, const std::string & origin) const {
587603
res.set_header("Access-Control-Allow-Origin", origin);
588604

589-
// Build methods string from config
590-
std::string methods_str;
591-
for (const auto & method : cors_config_.allowed_methods) {
592-
if (!methods_str.empty()) {
593-
methods_str += ", ";
594-
}
595-
methods_str += method;
605+
// Use pre-built header strings (built once in constructor)
606+
if (!cors_methods_header_.empty()) {
607+
res.set_header("Access-Control-Allow-Methods", cors_methods_header_);
596608
}
597-
if (!methods_str.empty()) {
598-
res.set_header("Access-Control-Allow-Methods", methods_str);
599-
}
600-
601-
// Build headers string from config
602-
std::string headers_str;
603-
for (const auto & header : cors_config_.allowed_headers) {
604-
if (!headers_str.empty()) {
605-
headers_str += ", ";
606-
}
607-
headers_str += header;
608-
}
609-
if (!headers_str.empty()) {
610-
res.set_header("Access-Control-Allow-Headers", headers_str);
609+
if (!cors_headers_header_.empty()) {
610+
res.set_header("Access-Control-Allow-Headers", cors_headers_header_);
611611
}
612612

613613
// Set credentials header if enabled
614614
if (cors_config_.allow_credentials) {
615615
res.set_header("Access-Control-Allow-Credentials", "true");
616616
}
617-
618-
// Set max age
619-
res.set_header("Access-Control-Max-Age", std::to_string(cors_config_.max_age_seconds));
620617
}
621618

622619
bool RESTServer::is_origin_allowed(const std::string & origin) const {

0 commit comments

Comments
 (0)