From 2dea44acf6c114f02f3469358222e084d1355e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= Date: Mon, 24 Aug 2026 21:50:48 +0800 Subject: [PATCH] fix: prevent rate limit bypass in public file shares (#13632) --- core/init/proxy/proxy.go | 96 ++++++++++++++++++++++++++++---- core/middleware/helper.go | 15 ++--- core/utils/clientip/clientip.go | 51 +++++++++++++++++ core/utils/publicshare/routes.go | 12 ++++ 4 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 core/utils/clientip/clientip.go create mode 100644 core/utils/publicshare/routes.go diff --git a/core/init/proxy/proxy.go b/core/init/proxy/proxy.go index 6c91811a3..eb4bbdc0f 100644 --- a/core/init/proxy/proxy.go +++ b/core/init/proxy/proxy.go @@ -5,7 +5,12 @@ import ( "net" "net/http" "net/http/httputil" + "slices" + "strings" "time" + + "github.com/1Panel-dev/1Panel/core/utils/clientip" + "github.com/1Panel-dev/1Panel/core/utils/publicshare" ) const SockPath = "/etc/1panel/agent.sock" @@ -28,20 +33,26 @@ func Init() { MaxIdleConnsPerHost: 50, IdleConnTimeout: 30 * time.Second, } - LocalAgentProxy = &httputil.ReverseProxy{ - Director: func(req *http.Request) { - if req.Header.Get("X-Forwarded-Proto") == "" { - if req.TLS != nil { - req.Header.Set("X-Forwarded-Proto", "https") - } else { - req.Header.Set("X-Forwarded-Proto", "http") - } + LocalAgentProxy = newLocalAgentProxy(transport) +} + +func newLocalAgentProxy(transport http.RoundTripper) *httputil.ReverseProxy { + return &httputil.ReverseProxy{ + Rewrite: func(proxyReq *httputil.ProxyRequest) { + if proxyReq.In.Form == nil { + proxyReq.Out.URL.RawQuery = proxyReq.In.URL.RawQuery } - if req.Header.Get("X-Forwarded-Host") == "" && req.Host != "" { - req.Header.Set("X-Forwarded-Host", req.Host) + if publicshare.IsAPI(proxyReq.In.URL.Path) { + proxyReq.SetXForwarded() + clientip.ReplaceForwardingHeaders( + proxyReq.Out.Header, + clientip.FromRemoteAddr(proxyReq.In.RemoteAddr), + ) + } else { + restoreLegacyForwardingHeaders(proxyReq) } - req.URL.Scheme = "http" - req.URL.Host = "unix" + proxyReq.Out.URL.Scheme = "http" + proxyReq.Out.URL.Host = "unix" }, Transport: transport, ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { @@ -50,3 +61,64 @@ func Init() { }, } } + +func restoreLegacyForwardingHeaders(proxyReq *httputil.ProxyRequest) { + restoreInboundHeader(proxyReq, "Forwarded") + restoreLegacyForwardedFor(proxyReq) + restoreLegacyForwardedMetadata(proxyReq, "X-Forwarded-Host", proxyReq.In.Host) + + forwardedProto := "http" + if proxyReq.In.TLS != nil { + forwardedProto = "https" + } + restoreLegacyForwardedMetadata(proxyReq, "X-Forwarded-Proto", forwardedProto) +} + +func restoreLegacyForwardedFor(proxyReq *httputil.ProxyRequest) { + restoreInboundHeader(proxyReq, "X-Forwarded-For") + clientIP, _, err := net.SplitHostPort(proxyReq.In.RemoteAddr) + if err != nil { + return + } + prior, ok := proxyReq.Out.Header["X-Forwarded-For"] + omit := ok && prior == nil + if len(prior) > 0 { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + if !omit { + proxyReq.Out.Header.Set("X-Forwarded-For", clientIP) + } +} + +func restoreLegacyForwardedMetadata(proxyReq *httputil.ProxyRequest, name, fallback string) { + if !isConnectionHeader(proxyReq.In.Header, name) && proxyReq.In.Header.Get(name) != "" { + restoreInboundHeader(proxyReq, name) + return + } + proxyReq.Out.Header.Del(name) + if fallback != "" { + proxyReq.Out.Header.Set(name, fallback) + } +} + +func restoreInboundHeader(proxyReq *httputil.ProxyRequest, name string) { + proxyReq.Out.Header.Del(name) + if isConnectionHeader(proxyReq.In.Header, name) { + return + } + canonicalName := http.CanonicalHeaderKey(name) + if values, ok := proxyReq.In.Header[canonicalName]; ok { + proxyReq.Out.Header[canonicalName] = slices.Clone(values) + } +} + +func isConnectionHeader(header http.Header, name string) bool { + for _, value := range header.Values("Connection") { + for _, token := range strings.Split(value, ",") { + if strings.EqualFold(strings.TrimSpace(token), name) { + return true + } + } + } + return false +} diff --git a/core/middleware/helper.go b/core/middleware/helper.go index b46ca99e4..4a67f1113 100644 --- a/core/middleware/helper.go +++ b/core/middleware/helper.go @@ -1,6 +1,10 @@ package middleware -import "strings" +import ( + "strings" + + "github.com/1Panel-dev/1Panel/core/utils/publicshare" +) func ShouldProxyToAgent(reqPath string) bool { if strings.HasPrefix(reqPath, "/1panel/swagger") || !strings.HasPrefix(reqPath, "/api/v2") { @@ -13,12 +17,5 @@ func ShouldProxyToAgent(reqPath string) bool { } func IsPublicFileShareAPI(reqPath string) bool { - switch reqPath { - case "/api/v2/files/share/info", - "/api/v2/files/share/check", - "/api/v2/files/share/download": - return true - default: - return false - } + return publicshare.IsAPI(reqPath) } diff --git a/core/utils/clientip/clientip.go b/core/utils/clientip/clientip.go new file mode 100644 index 000000000..17cf0c061 --- /dev/null +++ b/core/utils/clientip/clientip.go @@ -0,0 +1,51 @@ +package clientip + +import ( + "net" + "net/http" + "strings" +) + +const ( + forwardedHeader = "Forwarded" + forwardedForHeader = "X-Forwarded-For" + realIPHeader = "X-Real-IP" + panelClientIPHeader = "X-Panel-Client-IP" +) + +func FromRemoteAddr(remoteAddr string) string { + host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr)) + if err != nil { + return "" + } + ip := net.ParseIP(strings.TrimSpace(host)) + if ip == nil { + return "" + } + return ip.String() +} + +func ReplaceForwardingHeaders(header http.Header, clientIP string) { + if header == nil { + return + } + RemoveForwardingHeaders(header) + + ip := net.ParseIP(strings.TrimSpace(clientIP)) + if ip == nil { + return + } + normalized := ip.String() + header.Set(forwardedForHeader, normalized) + header.Set(realIPHeader, normalized) +} + +func RemoveForwardingHeaders(header http.Header) { + if header == nil { + return + } + header.Del(forwardedHeader) + header.Del(forwardedForHeader) + header.Del(realIPHeader) + header.Del(panelClientIPHeader) +} diff --git a/core/utils/publicshare/routes.go b/core/utils/publicshare/routes.go new file mode 100644 index 000000000..f90f575f6 --- /dev/null +++ b/core/utils/publicshare/routes.go @@ -0,0 +1,12 @@ +package publicshare + +func IsAPI(path string) bool { + switch path { + case "/api/v2/files/share/info", + "/api/v2/files/share/check", + "/api/v2/files/share/download": + return true + default: + return false + } +}