fix(mcp): Host 白名单加入 APP_BASE_URL 域名
rmcp 的 DNS-rebinding 防护默认仅允许 localhost/127.0.0.1/::1, 生产反代 proxy_set_header Host $host 会把真实域名转发进来被 403。 现把 APP_BASE_URL 的 host 解析加入 allowed_hosts(保留默认值), 本地直连与生产域名同时放行。 注:本次只修服务端潜在 prod 403;用户当前 0.0.0.0 的 403 是客户端 配置误用了 bind 地址,应改用 localhost/域名。
This commit is contained in:
parent
cc88a7e90a
commit
6d520c3d48
@ -1,11 +1,16 @@
|
||||
//! 构建并返回挂载于 `/mcp` 的 axum 子路由。
|
||||
//!
|
||||
//! 装配:
|
||||
//! - rmcp `StreamableHttpService`(无状态、JSON 响应、Origin 白名单);
|
||||
//! - 本 crate 的 `mcp_auth_middleware`(bearer → McpPrincipal,注入 extensions)。
|
||||
//! - rmcp `StreamableHttpService`(无状态、JSON 响应、Origin 白名单 + Host 白名单);
|
||||
//! - 本 crate 的 `mcp_auth_middleware`(bearer → McpPrincipal,注入 extensions);
|
||||
//!
|
||||
//! Origin 白名单:优先 `APP_BASE_URL`;否则放空(rmcp 默认对缺 Origin 放行)。
|
||||
//! 生产部署 MUST 设置 `APP_BASE_URL`(见 docs/DEPLOYMENT.md),白名单才会生效。
|
||||
//! 两道白名单都源自 `APP_BASE_URL`:
|
||||
//! - **Origin**:整串(含 scheme),rmcp 按 scheme+host 校验 `Origin` 头;
|
||||
//! - **Host**:仅 host 部分,加入 rmcp 的 DNS-rebinding 防护默认表(localhost/127.0.0.1/::1)。
|
||||
//! 生产反代 `proxy_set_header Host $host` 会把真实域名转发进来,必须加入否则 rmcp 一律 403。
|
||||
//!
|
||||
//! 未设置 `APP_BASE_URL` 则两者均空(rmcp 对缺 Origin 放行;Host 回退默认表仅本地可用)。
|
||||
//! 生产部署 MUST 设置 `APP_BASE_URL`(见 docs/DEPLOYMENT.md)。
|
||||
//! 协议版本头、体积上限(4MiB 默认)由 rmcp 内置,无需此处重复。
|
||||
|
||||
use axum::middleware::from_fn;
|
||||
@ -20,17 +25,37 @@ use crate::mcp::server::YggMcpServer;
|
||||
///
|
||||
/// 仅在 server feature 下有意义;WASM 构建不会调用本函数。
|
||||
pub fn mcp_route() -> Router {
|
||||
// 计算 Origin 白名单:APP_BASE_URL 规范化后加入;未设置则空(rmcp 对缺 Origin 放行)。
|
||||
let allowed_origins: Vec<String> = std::env::var("APP_BASE_URL")
|
||||
// 读取 APP_BASE_URL:同时用于 Origin 白名单(整串含 scheme)与 Host 白名单(仅 host)。
|
||||
let base_url = std::env::var("APP_BASE_URL")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.into_iter()
|
||||
.collect();
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
// Origin 白名单:APP_BASE_URL 整串(含 scheme),rmcp 按 scheme+host[:port] 匹配 Origin 头。
|
||||
// 未设置则空——rmcp 对缺 Origin 头的请求放行(大多数原生 MCP 客户端不发 Origin)。
|
||||
let allowed_origins: Vec<String> = base_url.iter().cloned().collect();
|
||||
|
||||
// Host 白名单(rmcp 的 DNS-rebinding 防护)。默认仅允许 localhost/127.0.0.1/::1:
|
||||
// 生产反代(nginx `proxy_set_header Host $host`)会把真实域名转发进来,若不把
|
||||
// APP_BASE_URL 的 host 也加入,所有 MCP 请求会被 rmcp 以 403 拒绝。保留默认值,
|
||||
// 确保本地直连(localhost)仍可用;只取 host(不含 port),rmcp 对无 port 条目匹配任意 port。
|
||||
let mut allowed_hosts: Vec<String> =
|
||||
vec!["localhost".into(), "127.0.0.1".into(), "::1".into()];
|
||||
if let Some(base) = &base_url {
|
||||
if let Ok(uri) = http::Uri::try_from(base.as_str()) {
|
||||
if let Some(authority) = uri.authority() {
|
||||
let host = authority.host().to_lowercase();
|
||||
if !allowed_hosts.contains(&host) {
|
||||
allowed_hosts.push(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = StreamableHttpServerConfig::default()
|
||||
.with_legacy_session_mode(false) // 无状态(SEP-2567)
|
||||
.with_json_response(true); // 简单工具用 application/json 直回
|
||||
.with_json_response(true) // 简单工具用 application/json 直回
|
||||
.with_allowed_hosts(allowed_hosts.iter().map(|s| s.as_str()));
|
||||
if !allowed_origins.is_empty() {
|
||||
config = config.with_allowed_origins(allowed_origins.iter().map(|s| s.as_str()));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user