release(secrets-mcp): 0.5.5 — 生产 CORS 显式 allow_methods,修复 tower-http 启动 panic
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 4m59s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 6s

credentials + wildcard methods/headers 被 tower-http 禁止;生产环境改为 GET/POST/PATCH/DELETE/OPTIONS 白名单。
This commit is contained in:
voson
2026-04-05 12:26:11 +08:00
parent 9d6ac5c13a
commit 2b994141b8
3 changed files with 29 additions and 5 deletions

2
Cargo.lock generated
View File

@@ -2066,7 +2066,7 @@ dependencies = [
[[package]] [[package]]
name = "secrets-mcp" name = "secrets-mcp"
version = "0.5.4" version = "0.5.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"askama", "askama",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "secrets-mcp" name = "secrets-mcp"
version = "0.5.4" version = "0.5.5"
edition.workspace = true edition.workspace = true
[[bin]] [[bin]]

View File

@@ -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. /// Build the CORS layer for the application.
/// ///
/// In production mode the origin is restricted to the BASE_URL origin /// In production mode the origin is restricted to the BASE_URL origin
/// (scheme://host:port, path stripped) and credentials are allowed. /// (scheme://host:port, path stripped) and credentials are allowed.
/// `allow_headers` uses an explicit whitelist to avoid the tower-http /// `allow_headers` and `allow_methods` use explicit whitelists to avoid the
/// restriction on `allow_credentials(true)` + `allow_headers(Any)`. /// tower-http restriction on `allow_credentials(true)` + wildcards.
/// ///
/// In development mode all origins, methods and headers are allowed. /// In development mode all origins, methods and headers are allowed.
fn build_cors_layer(base_url: &str, is_production: bool) -> CorsLayer { 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() CorsLayer::new()
.allow_origin(allowed_origin) .allow_origin(allowed_origin)
.allow_methods(Any) .allow_methods(production_allowed_methods())
.allow_headers(production_allowed_headers()) .allow_headers(production_allowed_headers())
.allow_credentials(true) .allow_credentials(true)
} else { } else {
@@ -304,6 +318,16 @@ mod tests {
assert!(names.contains(&"x-mcp-session")); 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] #[test]
fn production_cors_normalizes_base_url_with_path() { fn production_cors_normalizes_base_url_with_path() {
let url = url::Url::parse("https://secrets.example.com/secrets/app").unwrap(); let url = url::Url::parse("https://secrets.example.com/secrets/app").unwrap();