diff --git a/Cargo.lock b/Cargo.lock index ff75b1f..5c321ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2066,7 +2066,7 @@ dependencies = [ [[package]] name = "secrets-mcp" -version = "0.5.4" +version = "0.5.5" dependencies = [ "anyhow", "askama", diff --git a/crates/secrets-mcp/Cargo.toml b/crates/secrets-mcp/Cargo.toml index af7bc61..fd2d85f 100644 --- a/crates/secrets-mcp/Cargo.toml +++ b/crates/secrets-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secrets-mcp" -version = "0.5.4" +version = "0.5.5" edition.workspace = true [[bin]] diff --git a/crates/secrets-mcp/src/main.rs b/crates/secrets-mcp/src/main.rs index 9679fc0..f6188ac 100644 --- a/crates/secrets-mcp/src/main.rs +++ b/crates/secrets-mcp/src/main.rs @@ -250,12 +250,26 @@ fn production_allowed_headers() -> [axum::http::HeaderName; 5] { ] } +/// Production CORS allowed methods. +/// +/// Keep this list explicit because tower-http rejects +/// `allow_credentials(true)` together with `allow_methods(Any)`. +fn production_allowed_methods() -> [axum::http::Method; 5] { + [ + axum::http::Method::GET, + axum::http::Method::POST, + axum::http::Method::PATCH, + axum::http::Method::DELETE, + axum::http::Method::OPTIONS, + ] +} + /// Build the CORS layer for the application. /// /// In production mode the origin is restricted to the BASE_URL origin /// (scheme://host:port, path stripped) and credentials are allowed. -/// `allow_headers` uses an explicit whitelist to avoid the tower-http -/// restriction on `allow_credentials(true)` + `allow_headers(Any)`. +/// `allow_headers` and `allow_methods` use explicit whitelists to avoid the +/// tower-http restriction on `allow_credentials(true)` + wildcards. /// /// In development mode all origins, methods and headers are allowed. fn build_cors_layer(base_url: &str, is_production: bool) -> CorsLayer { @@ -272,7 +286,7 @@ fn build_cors_layer(base_url: &str, is_production: bool) -> CorsLayer { }; CorsLayer::new() .allow_origin(allowed_origin) - .allow_methods(Any) + .allow_methods(production_allowed_methods()) .allow_headers(production_allowed_headers()) .allow_credentials(true) } else { @@ -304,6 +318,16 @@ mod tests { assert!(names.contains(&"x-mcp-session")); } + #[test] + fn production_cors_methods_include_all_required() { + let methods = production_allowed_methods(); + assert!(methods.contains(&axum::http::Method::GET)); + assert!(methods.contains(&axum::http::Method::POST)); + assert!(methods.contains(&axum::http::Method::PATCH)); + assert!(methods.contains(&axum::http::Method::DELETE)); + assert!(methods.contains(&axum::http::Method::OPTIONS)); + } + #[test] fn production_cors_normalizes_base_url_with_path() { let url = url::Url::parse("https://secrets.example.com/secrets/app").unwrap();