From 532bb623350d828881f59ba5a716c610a371356e Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Tue, 19 May 2026 19:05:13 +0800 Subject: [PATCH] feat: Enhance the security of local requests --- agent/init/router/router.go | 5 ++ agent/utils/req_helper/core.go | 96 +++++++++++++++++++++++++++-- core/app/api/v2/setting.go | 5 -- core/init/router/router.go | 5 ++ core/middleware/csrf_protect.go | 3 + core/middleware/local_req_check.go | 58 +++++++++++++++++ core/middleware/password_expired.go | 4 ++ core/router/ro_setting.go | 2 +- core/utils/common/local_token.go | 69 +++++++++++++++++++++ 9 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 core/middleware/local_req_check.go create mode 100644 core/utils/common/local_token.go diff --git a/agent/init/router/router.go b/agent/init/router/router.go index 9ecaa9813987..10008ec318ed 100644 --- a/agent/init/router/router.go +++ b/agent/init/router/router.go @@ -15,6 +15,7 @@ var ( func Routers() *gin.Engine { Router = gin.Default() + configureTrustedProxies(Router) Router.Use(i18n.UseI18n()) PrivateGroup := Router.Group("/api/v2") @@ -29,3 +30,7 @@ func Routers() *gin.Engine { return Router } + +func configureTrustedProxies(router *gin.Engine) { + _ = router.SetTrustedProxies(nil) +} diff --git a/agent/utils/req_helper/core.go b/agent/utils/req_helper/core.go index 800b22924cea..c6264953218b 100644 --- a/agent/utils/req_helper/core.go +++ b/agent/utils/req_helper/core.go @@ -2,13 +2,25 @@ package req_helper import ( "bytes" + "crypto/md5" + "crypto/rand" "crypto/tls" + "encoding/hex" + "encoding/json" "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" - "net/http" ) +const LocalTokenHeader = "X-Panel-Local-Token" + func PostLocalCore(url string) error { var serverPortSetting model.Setting _ = global.CoreDB.Model(&model.Setting{}).Where("key = ?", "ServerPort").First(&serverPortSetting).Error @@ -22,11 +34,6 @@ func PostLocalCore(url string) error { prefix = "https" } - reloadURL := fmt.Sprintf("%s://127.0.0.1:%s/api/v2%s", prefix, serverPortSetting.Value, url) - req, err := http.NewRequest("POST", reloadURL, bytes.NewBuffer([]byte{})) - if err != nil { - return err - } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } @@ -34,10 +41,87 @@ func PostLocalCore(url string) error { Transport: tr, } defer client.CloseIdleConnections() + return postLocalCoreTo(fmt.Sprintf("%s://127.0.0.1:%s", prefix, serverPortSetting.Value), url, client) +} + +func postLocalCoreTo(baseURL string, url string, client *http.Client) error { + token, err := ensureLocalToken() + if err != nil { + return err + } + if client == nil { + client = http.DefaultClient + } + + reloadURL := strings.TrimRight(baseURL, "/") + "/api/v2" + url + req, err := http.NewRequest(http.MethodPost, reloadURL, bytes.NewBuffer([]byte{})) + if err != nil { + return err + } + req.Header.Set(LocalTokenHeader, token) + resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("request local core failed: status %s", resp.Status) + } + + var result dto.Response + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + if result.Code != http.StatusOK { + return fmt.Errorf("request local core failed: code %d, message: %s", result.Code, result.Message) + } return nil } + +func ensureLocalToken() (string, error) { + secret, err := ensureLocalSecret() + if err != nil { + return "", err + } + return generateLocalToken(secret), nil +} + +func ensureLocalSecret() (string, error) { + secretPath := localSecretPath() + data, err := os.ReadFile(secretPath) + if err == nil && len(data) != 0 { + return string(data), nil + } + if err != nil && !os.IsNotExist(err) { + return "", err + } + + if err := os.MkdirAll(filepath.Dir(secretPath), 0700); err != nil { + return "", err + } + secretBytes := make([]byte, 32) + if _, err := rand.Read(secretBytes); err != nil { + return "", err + } + secret := hex.EncodeToString(secretBytes) + if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil { + return "", err + } + return secret, nil +} + +func localSecretPath() string { + tmpDir := global.Dir.TmpDir + if tmpDir == "" { + tmpDir = filepath.Join(global.CONF.Base.InstallDir, "1panel/tmp") + } + return filepath.Join(tmpDir, ".secret") +} + +func generateLocalToken(secret string) string { + today := time.Now().Format("2006-01-02") + h := md5.New() + h.Write([]byte(secret + "-" + today)) + return hex.EncodeToString(h.Sum(nil))[:16] +} diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index 22b5901b0ea3..b49c09363d97 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -364,11 +364,6 @@ func (b *BaseApi) UpdatePort(c *gin.Context) { } func (b *BaseApi) ReloadSSL(c *gin.Context) { - clientIP := c.ClientIP() - if clientIP != "127.0.0.1" { - helper.InternalServer(c, errors.New("only localhost can reload ssl")) - return - } if err := settingService.UpdateSystemSSL(); err != nil { helper.InternalServer(c, err) return diff --git a/core/init/router/router.go b/core/init/router/router.go index 8cb6fc97bb73..b11cfc4414b3 100644 --- a/core/init/router/router.go +++ b/core/init/router/router.go @@ -67,6 +67,7 @@ func setWebStatic(rootRouter *gin.RouterGroup) { func Routers() *gin.Engine { Router = gin.New() + configureTrustedProxies(Router) Router.Use(i18n.UseI18n()) Router.Use(middleware.WhiteAllow()) Router.Use(middleware.BindDomain()) @@ -109,6 +110,10 @@ func Routers() *gin.Engine { return Router } +func configureTrustedProxies(router *gin.Engine) { + _ = router.SetTrustedProxies(nil) +} + func RegisterImages(rootRouter *gin.RouterGroup) { staticDir := filepath.Join(global.CONF.Base.InstallDir, "1panel/uploads/theme") rootRouter.GET("/api/v2/images/*filename", func(c *gin.Context) { diff --git a/core/middleware/csrf_protect.go b/core/middleware/csrf_protect.go index c6f3135261df..31afa66b298b 100644 --- a/core/middleware/csrf_protect.go +++ b/core/middleware/csrf_protect.go @@ -33,6 +33,9 @@ func CSRFTokenGuard() gin.HandlerFunc { } func requiresCSRFTokenCheck(c *gin.Context) bool { + if IsLocalRequest(c) { + return false + } unsafeMethod := c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead && c.Request.Method != http.MethodOptions && diff --git a/core/middleware/local_req_check.go b/core/middleware/local_req_check.go new file mode 100644 index 000000000000..3059d12fad88 --- /dev/null +++ b/core/middleware/local_req_check.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/gin-gonic/gin" +) + +const ( + localRequestContextKey = "LOCAL_REQUEST" + localSSLReloadPath = "/api/v2/core/settings/ssl/reload" +) + +func LocalReqCheck() gin.HandlerFunc { + return func(c *gin.Context) { + clientIP := common.GetRealClientIP(c) + if !isLocalClientIP(clientIP) { + abortLocalRequest(c, fmt.Sprintf("invalid client ip: %s", clientIP)) + return + } + if !common.ValidateLocalToken(c.GetHeader(common.LocalTokenHeader)) { + abortLocalRequest(c, "local token invalid") + return + } + c.Next() + } +} + +func IsLocalRequest(c *gin.Context) bool { + if c.GetBool(localRequestContextKey) { + return true + } + if c.Request.URL.Path != localSSLReloadPath { + return false + } + if !isLocalClientIP(common.GetRealClientIP(c)) { + return false + } + if !common.ValidateLocalToken(c.GetHeader(common.LocalTokenHeader)) { + return false + } + c.Set(localRequestContextKey, true) + return true +} + +func isLocalClientIP(clientIP string) bool { + return clientIP == "127.0.0.1" || clientIP == "::1" +} + +func abortLocalRequest(c *gin.Context, message string) { + c.AbortWithStatusJSON(http.StatusForbidden, dto.Response{ + Code: http.StatusForbidden, + Message: message, + }) +} diff --git a/core/middleware/password_expired.go b/core/middleware/password_expired.go index 4ed3241bfc3d..48f5f0964346 100644 --- a/core/middleware/password_expired.go +++ b/core/middleware/password_expired.go @@ -15,6 +15,10 @@ import ( func PasswordExpired() gin.HandlerFunc { return func(c *gin.Context) { + if IsLocalRequest(c) { + c.Next() + return + } if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") || c.Request.URL.Path == "/api/v2/core/settings/search" || c.Request.URL.Path == "/api/v2/core/settings/search/base" || diff --git a/core/router/ro_setting.go b/core/router/ro_setting.go index 7aad0985643a..a369efa8708c 100644 --- a/core/router/ro_setting.go +++ b/core/router/ro_setting.go @@ -39,7 +39,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { settingRouter.GET("/upgrade/releases", baseApi.LoadRelease) settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) - noAuthRouter.POST("/ssl/reload", baseApi.ReloadSSL) + noAuthRouter.POST("/ssl/reload", middleware.LocalReqCheck(), baseApi.ReloadSSL) settingRouter.POST("/apps/store/update", baseApi.UpdateAppstoreConfig) settingRouter.GET("/apps/store/config", baseApi.GetAppstoreConfig) diff --git a/core/utils/common/local_token.go b/core/utils/common/local_token.go new file mode 100644 index 000000000000..ceb692df0ac3 --- /dev/null +++ b/core/utils/common/local_token.go @@ -0,0 +1,69 @@ +package common + +import ( + "crypto/md5" + "crypto/rand" + "crypto/subtle" + "encoding/hex" + "os" + "path/filepath" + "time" + + "github.com/1Panel-dev/1Panel/core/global" +) + +const LocalTokenHeader = "X-Panel-Local-Token" + +func EnsureLocalToken() (string, error) { + secret, err := ensureLocalSecret() + if err != nil { + return "", err + } + return generateLocalToken(secret), nil +} + +func ValidateLocalToken(token string) bool { + if token == "" { + return false + } + expected, err := EnsureLocalToken() + if err != nil { + return false + } + return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1 +} + +func ensureLocalSecret() (string, error) { + secretPath := localSecretPath() + data, err := os.ReadFile(secretPath) + if err == nil && len(data) != 0 { + return string(data), nil + } + if err != nil && !os.IsNotExist(err) { + return "", err + } + + if err := os.MkdirAll(filepath.Dir(secretPath), 0700); err != nil { + return "", err + } + secretBytes := make([]byte, 32) + if _, err := rand.Read(secretBytes); err != nil { + return "", err + } + secret := hex.EncodeToString(secretBytes) + if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil { + return "", err + } + return secret, nil +} + +func localSecretPath() string { + return filepath.Join(global.CONF.Base.InstallDir, "1panel/tmp/.secret") +} + +func generateLocalToken(secret string) string { + today := time.Now().Format("2006-01-02") + h := md5.New() + h.Write([]byte(secret + "-" + today)) + return hex.EncodeToString(h.Sum(nil))[:16] +}