mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
fix(security): patch SSRF and info disclosure vulnerabilities
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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"))
|
response.WriteJSON(w, response.ErrDefault("Remote URL and Token are required"))
|
||||||
return
|
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")
|
domainCfg, _ := h.repo.GetConfigByName("panel_domain")
|
||||||
localDomain := ""
|
localDomain := ""
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
repo *repo.Repository
|
repo *repo.Repository
|
||||||
jwtSecret string
|
jwtSecret string
|
||||||
@@ -356,6 +357,12 @@ func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
||||||
return
|
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)
|
cfg, err := h.repo.GetConfigByName(req.Name)
|
||||||
if err != nil {
|
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()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
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))
|
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()))
|
response.WriteJSON(w, response.ErrDefault("设备绑定失败: "+err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation might still fail with scope if we don't query via machine id, but since activate machine succeeded
|
// 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
|
// we can consider the license valid for our simple usecase
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -301,6 +301,10 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.ErrDefault("节点名称和地址不能为空"))
|
response.WriteJSON(w, response.ErrDefault("节点名称和地址不能为空"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := IsValidNodeAddress(serverIP); err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
inx := h.repo.NextIndex("node")
|
inx := h.repo.NextIndex("node")
|
||||||
@@ -365,6 +369,13 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
newHTTP := asInt(req["http"], currentHTTP)
|
newHTTP := asInt(req["http"], currentHTTP)
|
||||||
newTLS := asInt(req["tls"], currentTLS)
|
newTLS := asInt(req["tls"], currentTLS)
|
||||||
newSocks := asInt(req["socks"], currentSocks)
|
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 currentStatus == 1 && (newHTTP != currentHTTP || newTLS != currentTLS || newSocks != currentSocks) {
|
||||||
if err := h.applyNodeProtocolChange(id, newHTTP, newTLS, newSocks); err != nil {
|
if err := h.applyNodeProtocolChange(id, newHTTP, newTLS, newSocks); err != nil {
|
||||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
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()
|
now := time.Now().UnixMilli()
|
||||||
if err := h.repo.UpdateNode(id,
|
if err := h.repo.UpdateNode(id,
|
||||||
asString(req["name"]),
|
asString(req["name"]),
|
||||||
asString(req["serverIp"]),
|
serverIP,
|
||||||
nullableText(asString(req["serverIpV4"])),
|
nullableText(asString(req["serverIpV4"])),
|
||||||
nullableText(asString(req["serverIpV6"])),
|
nullableText(asString(req["serverIpV6"])),
|
||||||
defaultString(asString(req["port"]), "1000-65535"),
|
defaultString(asString(req["port"]), "1000-65535"),
|
||||||
@@ -1686,6 +1697,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if roleID != 0 {
|
if roleID != 0 {
|
||||||
|
if err := IsSafeRemoteAddr(remoteAddr); err != nil {
|
||||||
|
response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络或保留地址"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil {
|
if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil {
|
||||||
response.WriteJSON(w, response.Err(-1, "普通用户无法设置限速规则"))
|
response.WriteJSON(w, response.Err(-1, "普通用户无法设置限速规则"))
|
||||||
return
|
return
|
||||||
@@ -1805,6 +1820,12 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if remoteAddr == "" {
|
if remoteAddr == "" {
|
||||||
remoteAddr = forward.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"]))
|
strategy := strings.TrimSpace(asString(req["strategy"]))
|
||||||
if strategy == "" {
|
if strategy == "" {
|
||||||
strategy = forward.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
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ func requiresAdmin(path string) bool {
|
|||||||
return true
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"go-backend/internal/http/handler"
|
||||||
"go-backend/internal/store/repo"
|
"go-backend/internal/store/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
handler.DisableSafeRemoteAddrCheckForTesting = true
|
||||||
|
}
|
||||||
|
|
||||||
func mustLastInsertID(t *testing.T, r *repo.Repository, label string) int64 {
|
func mustLastInsertID(t *testing.T, r *repo.Repository, label string) int64 {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var id int64
|
var id int64
|
||||||
|
|||||||
Reference in New Issue
Block a user