fix(security): patch SSRF and info disclosure vulnerabilities

This commit is contained in:
sagitchu
2026-04-20 11:31:17 +08:00
parent 77b7f066f3
commit 8611748c46
6 changed files with 112 additions and 3 deletions
@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
@@ -635,6 +636,15 @@ func (h *Handler) nodeImport(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("Remote URL and Token are required"))
return
}
rURL, err := url.Parse(req.RemoteURL)
if err != nil || (rURL.Scheme != "http" && rURL.Scheme != "https") {
response.WriteJSON(w, response.ErrDefault("Invalid Remote URL format"))
return
}
if err := IsSafeRemoteAddr(rURL.Host); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将远程节点地址设置为内部网络或保留地址"))
return
}
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
localDomain := ""
+14 -1
View File
@@ -27,6 +27,7 @@ import (
"github.com/google/uuid"
)
type Handler struct {
repo *repo.Repository
jwtSecret string
@@ -356,6 +357,12 @@ func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
return
}
configName := strings.ToLower(strings.TrimSpace(req.Name))
switch configName {
case "license_key", "cloudflare_secret_key", "jwt_secret":
response.WriteJSON(w, response.Err(403, "禁止访问敏感配置"))
return
}
cfg, err := h.repo.GetConfigByName(req.Name)
if err != nil {
@@ -381,6 +388,12 @@ func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
ctxClaims := r.Context().Value(middleware.ClaimsContextKey)
if claims, ok := ctxClaims.(auth.Claims); !ok || claims.RoleID != 0 {
delete(cfgMap, "license_key")
delete(cfgMap, "cloudflare_secret_key")
delete(cfgMap, "jwt_secret")
}
response.WriteJSON(w, response.OK(cfgMap))
}
@@ -852,7 +865,7 @@ func (h *Handler) licenseActivate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("设备绑定失败: "+err.Error()))
return
}
// Validation might still fail with scope if we don't query via machine id, but since activate machine succeeded
// we can consider the license valid for our simple usecase
} else {
+22 -1
View File
@@ -301,6 +301,10 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("节点名称和地址不能为空"))
return
}
if err := IsValidNodeAddress(serverIP); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
now := time.Now().UnixMilli()
inx := h.repo.NextIndex("node")
@@ -365,6 +369,13 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
newHTTP := asInt(req["http"], currentHTTP)
newTLS := asInt(req["tls"], currentTLS)
newSocks := asInt(req["socks"], currentSocks)
serverIP := asString(req["serverIp"])
if serverIP != "" {
if err := IsValidNodeAddress(serverIP); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
}
if currentStatus == 1 && (newHTTP != currentHTTP || newTLS != currentTLS || newSocks != currentSocks) {
if err := h.applyNodeProtocolChange(id, newHTTP, newTLS, newSocks); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
@@ -375,7 +386,7 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
now := time.Now().UnixMilli()
if err := h.repo.UpdateNode(id,
asString(req["name"]),
asString(req["serverIp"]),
serverIP,
nullableText(asString(req["serverIpV4"])),
nullableText(asString(req["serverIpV6"])),
defaultString(asString(req["port"]), "1000-65535"),
@@ -1686,6 +1697,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
return
}
if roleID != 0 {
if err := IsSafeRemoteAddr(remoteAddr); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络或保留地址"))
return
}
if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil {
response.WriteJSON(w, response.Err(-1, "普通用户无法设置限速规则"))
return
@@ -1805,6 +1820,12 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
if remoteAddr == "" {
remoteAddr = forward.RemoteAddr
}
if actorRole != 0 {
if err := IsSafeRemoteAddr(remoteAddr); err != nil {
response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络或保留地址"))
return
}
}
strategy := strings.TrimSpace(asString(req["strategy"]))
if strategy == "" {
strategy = forward.Strategy
@@ -0,0 +1,60 @@
package handler
import (
"fmt"
"net"
"strings"
)
// DisableSafeRemoteAddrCheckForTesting allows bypassing the safety check during integration tests.
var DisableSafeRemoteAddrCheckForTesting = false
// IsSafeRemoteAddr checks if a given address is safe to connect to (prevents SSRF/Open Proxy).
// It resolves domains to IPs to prevent DNS rebinding attacks pointing to internal networks.
func IsSafeRemoteAddr(addr string) error {
if DisableSafeRemoteAddrCheckForTesting {
return nil
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
if strings.Contains(err.Error(), "missing port in address") {
host = addr
} else {
return fmt.Errorf("invalid address format: %v", err)
}
}
ips, err := net.LookupIP(host)
if err != nil {
return fmt.Errorf("could not resolve address: %v", err)
}
for _, ip := range ips {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast() {
return fmt.Errorf("address resolves to internal or reserved IP: %s", ip.String())
}
}
return nil
}
// IsValidNodeAddress ensures the address is strictly a host or host:port.
// It explicitly denies schemes (http://, https://), paths (/...), and query params (?).
func IsValidNodeAddress(addr string) error {
addr = strings.TrimSpace(addr)
if strings.Contains(addr, "://") {
return fmt.Errorf("address must not contain scheme (e.g. http://)")
}
if strings.ContainsAny(addr, "/?") {
return fmt.Errorf("address must not contain path or query parameters")
}
_, _, err := net.SplitHostPort(addr)
if err != nil {
if !strings.Contains(err.Error(), "missing port in address") {
return fmt.Errorf("invalid address format")
}
}
return nil
}
+1 -1
View File
@@ -109,7 +109,7 @@ func requiresAdmin(path string) bool {
return true
}
if strings.HasPrefix(path, "/api/v1/federation/share/") {
if strings.HasPrefix(path, "/api/v1/federation/share/") || strings.HasPrefix(path, "/api/v1/federation/node/") {
return true
}
@@ -6,9 +6,14 @@ import (
"strings"
"testing"
"go-backend/internal/http/handler"
"go-backend/internal/store/repo"
)
func init() {
handler.DisableSafeRemoteAddrCheckForTesting = true
}
func mustLastInsertID(t *testing.T, r *repo.Repository, label string) int64 {
t.Helper()
var id int64