fix(saas): 修复安全审查发现的 Critical/High/Medium 问题
- Critical: 移除注册接口的 role 字段,固定为 "user" 防止权限提升 - High: 生产环境未配置 cors_origins 时拒绝启动而非默认全开放 - Medium: 增强 SSRF 防护 — 阻止 IPv6 映射地址、私有 IP 网段、十进制 IP 格式
This commit is contained in:
@@ -228,12 +228,65 @@ fn validate_provider_url(url: &str) -> SaasResult<()> {
|
||||
Some(h) => h,
|
||||
None => return Err(SaasError::InvalidInput("provider URL 缺少 host".into())),
|
||||
};
|
||||
let blocked = ["127.0.0.1", "0.0.0.0", "localhost", "::1", "169.254.169.254", "metadata.google.internal"];
|
||||
for blocked_host in &blocked {
|
||||
if host == *blocked_host || host.ends_with(&format!(".{}", blocked_host)) {
|
||||
|
||||
// 精确匹配的阻止列表
|
||||
let blocked_exact = [
|
||||
"127.0.0.1", "0.0.0.0", "localhost", "::1", "::ffff:127.0.0.1",
|
||||
"0:0:0:0:0:ffff:7f00:1", "169.254.169.254", "metadata.google.internal",
|
||||
"10.0.0.1", "172.16.0.1", "192.168.0.1",
|
||||
];
|
||||
if blocked_exact.contains(&host) {
|
||||
return Err(SaasError::InvalidInput(format!("provider URL 指向禁止的内网地址: {}", host)));
|
||||
}
|
||||
|
||||
// 后缀匹配 (阻止子域名)
|
||||
let blocked_suffixes = ["localhost", "internal", "local", "localhost.localdomain"];
|
||||
for suffix in &blocked_suffixes {
|
||||
if host.ends_with(&format!(".{}", suffix)) {
|
||||
return Err(SaasError::InvalidInput(format!("provider URL 指向禁止的内网地址: {}", host)));
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止 IPv4 私有网段 (通过解析 IP)
|
||||
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||
if is_private_ip(&ip) {
|
||||
return Err(SaasError::InvalidInput(format!("provider URL 指向私有 IP 地址: {}", host)));
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止纯数字 host (可能是十进制 IP 表示法,如 2130706433 = 127.0.0.1)
|
||||
if host.parse::<u64>().is_ok() {
|
||||
return Err(SaasError::InvalidInput(format!("provider URL 使用了不允许的 IP 格式: {}", host)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查 IP 是否属于私有/内网地址范围
|
||||
fn is_private_ip(ip: &std::net::IpAddr) -> bool {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
let octets = v4.octets();
|
||||
// 10.0.0.0/8
|
||||
octets[0] == 10
|
||||
// 172.16.0.0/12
|
||||
|| (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31)
|
||||
// 192.168.0.0/16
|
||||
|| (octets[0] == 192 && octets[1] == 168)
|
||||
// 127.0.0.0/8 (loopback)
|
||||
|| octets[0] == 127
|
||||
// 169.254.0.0/16 (link-local)
|
||||
|| (octets[0] == 169 && octets[1] == 254)
|
||||
// 0.0.0.0/8
|
||||
|| octets[0] == 0
|
||||
}
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
// ::1 (loopback)
|
||||
v6.is_loopback()
|
||||
// ::ffff:x.x.x.x (IPv6-mapped IPv4)
|
||||
|| v6.to_ipv4_mapped().map_or(false, |v4| is_private_ip(&std::net::IpAddr::V4(v4)))
|
||||
// fe80::/10 (link-local)
|
||||
|| (v6.segments()[0] & 0xffc0) == 0xfe80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user