mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
fix(security): reserve dashboard hosts from NAT and re-check DDNS profile ownership at worker time
GHSA-x6fg-52vr-hj4w: NAT is a commonHandler any authenticated member can create, and newHTTPandGRPCMux matches r.Host against NAT before dispatching the dashboard/gRPC handlers, so a member could register the dashboard's own host as a NAT Domain and hijack global routing. Add IsReservedDashboardHost (InstallHost/ListenHost plus an operator-declared ReservedHosts list for reverse-proxy deployments), reject reserved hosts on create/update, and drop pre-planted records when building the startup cache. GHSA-39g2-8x68-pmx8: bind-time CheckPermission let a member pre-bind a DDNS profile ID that the victim would only create later. GetDDNSProvidersFromProfiles now re-validates ownership by server owner UID and skips foreign-owned profiles (UserID==0 is treated as a migration artifact, not an admin grant). Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
This commit is contained in:
@@ -2,12 +2,81 @@ package singleton
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/nezhahq/nezha/model"
|
||||
"github.com/nezhahq/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
// GHSA-x6fg-52vr-hj4w: NAT 是 commonHandler,任意认证成员可创建。newHTTPandGRPCMux
|
||||
// 在分发 dashboard/gRPC 之前先按 r.Host 命中 NAT,故成员若把 Domain 设成 dashboard
|
||||
// 自身 host 即可抢占全局路由(disabled 触发 DoS,enabled 把请求隧道到攻击者 agent)。
|
||||
// 把 dashboard 的 InstallHost、ListenHost 以及运维声明的 ReservedHosts 列为
|
||||
// 保留 host:create/update 时拒绝,启动建表时丢弃,确保补丁前已植入的恶意记录
|
||||
// 在升级后不再生效。每个 host 拆成 hostname 后比较(忽略端口与大小写),反代/
|
||||
// 默认端口下的端口变体也拦得住。反代部署时进程看不到对外域名,运维把它配进
|
||||
// Conf.ReservedHosts(逗号分隔)即可让此处覆盖到公网入口。
|
||||
func IsReservedDashboardHost(domain string) bool {
|
||||
if Conf == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
target := splitDashboardHostname(domain)
|
||||
if target == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
hosts := []string{Conf.InstallHost, Conf.ListenHost}
|
||||
hosts = append(hosts, strings.Split(Conf.ReservedHosts, ",")...)
|
||||
for _, host := range hosts {
|
||||
if reserved := splitDashboardHostname(host); reserved != "" && reserved == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// splitDashboardHostname 归一化为小写 hostname,对 bracketed IPv6([::1]、
|
||||
// [::1]:8008)与裸 host:port 一视同仁,避免 candidate 与 reserved 解析形态不
|
||||
// 一致导致漏拦。两类等价形态也必须收敛,否则 guard 放行而 r.Host 精确命中
|
||||
// 仍能劫持路由:
|
||||
// - DNS absolute name 的尾点(panel.example.com. 与 panel.example.com 指向同
|
||||
// 一主机),去掉单个尾点;
|
||||
// - IP literal 的压缩/展开写法(::1 与 0:0:0:0:0:0:0:1),用 netip 归一到
|
||||
// 规范文本。
|
||||
func splitDashboardHostname(host string) string {
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
if h, _, err := net.SplitHostPort(host); err == nil && h != "" {
|
||||
host = h
|
||||
} else {
|
||||
host = strings.Trim(host, "[]")
|
||||
}
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
if addr, err := netip.ParseAddr(host); err == nil {
|
||||
return addr.String()
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// filterReservedNATProfiles 丢弃 Domain 命中 dashboard 保留 host 的 NAT 记录,
|
||||
// 让 NewNATClass 启动建表时不把补丁前植入的劫持记录加载进路由表。
|
||||
func filterReservedNATProfiles(in []*model.NAT) []*model.NAT {
|
||||
out := in[:0]
|
||||
for _, profile := range in {
|
||||
if profile == nil || IsReservedDashboardHost(profile.Domain) {
|
||||
continue
|
||||
}
|
||||
out = append(out, profile)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type NATClass struct {
|
||||
class[string, *model.NAT]
|
||||
|
||||
@@ -18,6 +87,7 @@ func NewNATClass() *NATClass {
|
||||
var sortedList []*model.NAT
|
||||
|
||||
DB.Find(&sortedList)
|
||||
sortedList = filterReservedNATProfiles(sortedList)
|
||||
list := make(map[string]*model.NAT, len(sortedList))
|
||||
idToDomain := make(map[uint64]string, len(sortedList))
|
||||
for _, profile := range sortedList {
|
||||
|
||||
Reference in New Issue
Block a user