CVE-2025-53534
Description
RatPanel is a server operation and maintenance management panel. In versions 2.3.19 through 2.5.5, when an attacker obtains the backend login path of RatPanel (including but not limited to weak default paths, brute-force cracking, etc.), they can execute system commands or take over hosts managed by the panel without logging in. In addition to this remote code execution (RCE) vulnerability, the flawed code also leads to unauthorized access. RatPanel uses the CleanPath middleware provided by github.com/go-chi/chi package to clean URLs, but but the middleware does not process r.URL.Path, which can cause the paths to be misinterpreted. This is fixed in version 2.5.6.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/tnborg/panelGo | >= 2.3.19, < 2.5.6 | 2.5.6 |
github.com/tnborg/panelGo | >= 0.0.0-20241111062800-91ecd04c2700, < 0.0.0-20250707071915-4985eb2e1f38 | 0.0.0-20250707071915-4985eb2e1f38 |
Patches
6ed5c74c75342c4b84632b4b6c4b84632b4b63 files changed · +11 −19
internal/http/middleware/entrance.go+5 −7 modified@@ -29,8 +29,6 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu entrance = "/" + entrance } - routePath := chi.RouteContext(r.Context()).RoutePath - // 情况一:设置了绑定域名、IP、UA,且请求不符合要求,返回错误 host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -80,7 +78,7 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu } // 情况二:请求路径与入口路径相同或者未设置访问入口,标记通过验证并重定向到登录页面 - if (strings.TrimSuffix(routePath, "/") == entrance || entrance == "/") && + if (strings.TrimSuffix(r.URL.Path, "/") == entrance || entrance == "/") && r.Header.Get("Authorization") == "" { sess.Put("verify_entrance", true) render := chix.NewRender(w, r) @@ -90,12 +88,12 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu } // 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证 - if strings.HasPrefix(routePath, entrance) && r.Header.Get("Authorization") != "" { + if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" { // 只在设置了入口路径的情况下,才进行重写 if entrance != "/" { if rctx := chi.RouteContext(r.Context()); rctx != nil { - rctx.RoutePath = strings.TrimPrefix(routePath, entrance) - r.URL.Path = strings.TrimPrefix(routePath, entrance) + rctx.RoutePath = strings.TrimPrefix(rctx.RoutePath, entrance) + r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance) } } next.ServeHTTP(w, r) @@ -105,7 +103,7 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu // 情况四:非调试模式且未通过验证的请求,返回错误 if !conf.Bool("app.debug") && sess.Missing("verify_entrance") && - routePath != "/robots.txt" { + r.URL.Path != "/robots.txt" { Abort(w, http.StatusTeapot, t.Get("invalid access entrance")) return }
internal/http/middleware/must_install.go+4 −7 modified@@ -4,7 +4,6 @@ import ( "net/http" "strings" - "github.com/go-chi/chi/v5" "github.com/leonelquinteros/gotext" "github.com/tnb-labs/panel/internal/biz" @@ -14,15 +13,13 @@ import ( func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - routePath := chi.RouteContext(r.Context()).RoutePath - var slugs []string - if strings.HasPrefix(routePath, "/api/website") { + if strings.HasPrefix(r.URL.Path, "/api/website") { slugs = append(slugs, "nginx") - } else if strings.HasPrefix(routePath, "/api/container") { + } else if strings.HasPrefix(r.URL.Path, "/api/container") { slugs = append(slugs, "podman", "docker") - } else if strings.HasPrefix(routePath, "/api/apps/") { - pathArr := strings.Split(routePath, "/") + } else if strings.HasPrefix(r.URL.Path, "/api/apps/") { + pathArr := strings.Split(r.URL.Path, "/") if len(pathArr) < 4 { Abort(w, http.StatusForbidden, t.Get("app not found")) return
internal/http/middleware/must_login.go+2 −5 modified@@ -9,7 +9,6 @@ import ( "slices" "strings" - "github.com/go-chi/chi/v5" "github.com/go-rat/sessions" "github.com/leonelquinteros/gotext" "github.com/spf13/cast" @@ -36,18 +35,16 @@ func MustLogin(t *gotext.Locale, session *sessions.Manager, userToken biz.UserTo return } - routePath := chi.RouteContext(r.Context()).RoutePath - // 对白名单和非 API 请求放行 - if slices.Contains(whiteList, routePath) || !strings.HasPrefix(routePath, "/api") { + if slices.Contains(whiteList, r.URL.Path) || !strings.HasPrefix(r.URL.Path, "/api") { next.ServeHTTP(w, r) return } userID := uint(0) if r.Header.Get("Authorization") != "" { // 禁止访问 ws 相关的接口 - if strings.HasPrefix(routePath, "/api/ws") { + if strings.HasPrefix(r.URL.Path, "/api/ws") { Abort(w, http.StatusForbidden, t.Get("ws not allowed")) return }
4 files changed · +22 −14
internal/http/middleware/entrance.go+7 −5 modified@@ -29,6 +29,8 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu entrance = "/" + entrance } + routePath := chi.RouteContext(r.Context()).RoutePath + // 情况一:设置了绑定域名、IP、UA,且请求不符合要求,返回错误 host, _, err := net.SplitHostPort(r.Host) if err != nil { @@ -78,7 +80,7 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu } // 情况二:请求路径与入口路径相同或者未设置访问入口,标记通过验证并重定向到登录页面 - if (strings.TrimSuffix(r.URL.Path, "/") == entrance || entrance == "/") && + if (strings.TrimSuffix(routePath, "/") == entrance || entrance == "/") && r.Header.Get("Authorization") == "" { sess.Put("verify_entrance", true) render := chix.NewRender(w, r) @@ -88,12 +90,12 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu } // 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证 - if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" { + if strings.HasPrefix(routePath, entrance) && r.Header.Get("Authorization") != "" { // 只在设置了入口路径的情况下,才进行重写 if entrance != "/" { if rctx := chi.RouteContext(r.Context()); rctx != nil { - rctx.RoutePath = strings.TrimPrefix(rctx.RoutePath, entrance) - r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance) + rctx.RoutePath = strings.TrimPrefix(routePath, entrance) + r.URL.Path = strings.TrimPrefix(routePath, entrance) } } next.ServeHTTP(w, r) @@ -103,7 +105,7 @@ func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) fu // 情况四:非调试模式且未通过验证的请求,返回错误 if !conf.Bool("app.debug") && sess.Missing("verify_entrance") && - r.URL.Path != "/robots.txt" { + routePath != "/robots.txt" { Abort(w, http.StatusTeapot, t.Get("invalid access entrance")) return }
internal/http/middleware/middleware.go+3 −3 modified@@ -39,16 +39,16 @@ func NewMiddlewares(conf *koanf.Koanf, log *slog.Logger, session *sessions.Manag // Globals is a collection of global middleware that will be applied to every request. func (r *Middlewares) Globals(t *gotext.Locale, mux *chi.Mux) []func(http.Handler) http.Handler { return []func(http.Handler) http.Handler{ - sessionmiddleware.StartSession(r.session), + middleware.Recoverer, //middleware.SupressNotFound(mux),// bug https://github.com/go-chi/chi/pull/940 middleware.CleanPath, middleware.StripSlashes, - middleware.Compress(5), httplog.RequestLogger(r.log, &httplog.Options{ Level: slog.LevelInfo, LogRequestHeaders: []string{"User-Agent"}, }), - middleware.Recoverer, + middleware.Compress(5), + sessionmiddleware.StartSession(r.session), Status(t), Entrance(t, r.conf, r.session), MustLogin(t, r.session, r.userToken),
internal/http/middleware/must_install.go+7 −4 modified@@ -4,6 +4,7 @@ import ( "net/http" "strings" + "github.com/go-chi/chi/v5" "github.com/leonelquinteros/gotext" "github.com/tnb-labs/panel/internal/biz" @@ -13,13 +14,15 @@ import ( func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + routePath := chi.RouteContext(r.Context()).RoutePath + var slugs []string - if strings.HasPrefix(r.URL.Path, "/api/website") { + if strings.HasPrefix(routePath, "/api/website") { slugs = append(slugs, "nginx") - } else if strings.HasPrefix(r.URL.Path, "/api/container") { + } else if strings.HasPrefix(routePath, "/api/container") { slugs = append(slugs, "podman", "docker") - } else if strings.HasPrefix(r.URL.Path, "/api/apps/") { - pathArr := strings.Split(r.URL.Path, "/") + } else if strings.HasPrefix(routePath, "/api/apps/") { + pathArr := strings.Split(routePath, "/") if len(pathArr) < 4 { Abort(w, http.StatusForbidden, t.Get("app not found")) return
internal/http/middleware/must_login.go+5 −2 modified@@ -9,6 +9,7 @@ import ( "slices" "strings" + "github.com/go-chi/chi/v5" "github.com/go-rat/sessions" "github.com/leonelquinteros/gotext" "github.com/spf13/cast" @@ -35,16 +36,18 @@ func MustLogin(t *gotext.Locale, session *sessions.Manager, userToken biz.UserTo return } + routePath := chi.RouteContext(r.Context()).RoutePath + // 对白名单和非 API 请求放行 - if slices.Contains(whiteList, r.URL.Path) || !strings.HasPrefix(r.URL.Path, "/api") { + if slices.Contains(whiteList, routePath) || !strings.HasPrefix(routePath, "/api") { next.ServeHTTP(w, r) return } userID := uint(0) if r.Header.Get("Authorization") != "" { // 禁止访问 ws 相关的接口 - if strings.HasPrefix(r.URL.Path, "/api/ws") { + if strings.HasPrefix(routePath, "/api/ws") { Abort(w, http.StatusForbidden, t.Get("ws not allowed")) return }
3 files changed · +28 −28
internal/http/middleware/middleware.go+2 −1 modified@@ -24,8 +24,9 @@ func GlobalMiddleware() []func(http.Handler) http.Handler { LogRequestHeaders: []string{"User-Agent"}, }), middleware.Recoverer, - Entrance, Status, + Entrance, + MustLogin, MustInstall, } }
internal/http/middleware/must_login.go+16 −0 modified@@ -3,6 +3,8 @@ package middleware import ( "context" "net/http" + "slices" + "strings" "github.com/go-rat/chix" "github.com/spf13/cast" @@ -12,6 +14,14 @@ import ( // MustLogin 确保已登录 func MustLogin(next http.Handler) http.Handler { + // 白名单 + whiteList := []string{ + "/api/user/login", + "/api/user/logout", + "/api/user/isLogin", + "/api/dashboard/panel", + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess, err := app.Session.GetSession(r) if err != nil { @@ -22,6 +32,12 @@ func MustLogin(next http.Handler) http.Handler { }) } + // 对白名单和非 API 请求放行 + if slices.Contains(whiteList, r.URL.Path) || !strings.HasPrefix(r.URL.Path, "/api") { + next.ServeHTTP(w, r) + return + } + if sess.Missing("user_id") { render := chix.NewRender(w) render.Status(http.StatusUnauthorized)
internal/route/http.go+10 −27 modified@@ -21,25 +21,24 @@ func Http(r chi.Router) { r.With(middleware.Throttle(5, time.Minute)).Post("/login", user.Login) r.Post("/logout", user.Logout) r.Get("/isLogin", user.IsLogin) - r.With(middleware.MustLogin).Get("/info", user.Info) + r.Get("/info", user.Info) }) r.Route("/dashboard", func(r chi.Router) { dashboard := service.NewDashboardService() r.Get("/panel", dashboard.Panel) - r.With(middleware.MustLogin).Get("/homeApps", dashboard.HomeApps) - r.With(middleware.MustLogin).Post("/current", dashboard.Current) - r.With(middleware.MustLogin).Get("/systemInfo", dashboard.SystemInfo) - r.With(middleware.MustLogin).Get("/countInfo", dashboard.CountInfo) - r.With(middleware.MustLogin).Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp) - r.With(middleware.MustLogin).Get("/checkUpdate", dashboard.CheckUpdate) - r.With(middleware.MustLogin).Get("/updateInfo", dashboard.UpdateInfo) - r.With(middleware.MustLogin).Post("/update", dashboard.Update) - r.With(middleware.MustLogin).Post("/restart", dashboard.Restart) + r.Get("/homeApps", dashboard.HomeApps) + r.Post("/current", dashboard.Current) + r.Get("/systemInfo", dashboard.SystemInfo) + r.Get("/countInfo", dashboard.CountInfo) + r.Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp) + r.Get("/checkUpdate", dashboard.CheckUpdate) + r.Get("/updateInfo", dashboard.UpdateInfo) + r.Post("/update", dashboard.Update) + r.Post("/restart", dashboard.Restart) }) r.Route("/task", func(r chi.Router) { - r.Use(middleware.MustLogin) task := service.NewTaskService() r.Get("/status", task.Status) r.Get("/", task.List) @@ -48,7 +47,6 @@ func Http(r chi.Router) { }) r.Route("/website", func(r chi.Router) { - r.Use(middleware.MustLogin) website := service.NewWebsiteService() r.Get("/defaultConfig", website.GetDefaultConfig) r.Post("/defaultConfig", website.UpdateDefaultConfig) @@ -65,7 +63,6 @@ func Http(r chi.Router) { }) r.Route("/database", func(r chi.Router) { - r.Use(middleware.MustLogin) database := service.NewDatabaseService() r.Get("/", database.List) r.Post("/", database.Create) @@ -74,7 +71,6 @@ func Http(r chi.Router) { }) r.Route("/databaseServer", func(r chi.Router) { - r.Use(middleware.MustLogin) database := service.NewDatabaseService() r.Get("/", database.List) r.Post("/", database.Create) @@ -83,7 +79,6 @@ func Http(r chi.Router) { }) r.Route("/backup", func(r chi.Router) { - r.Use(middleware.MustLogin) backup := service.NewBackupService() r.Get("/{type}", backup.List) r.Post("/{type}", backup.Create) @@ -93,7 +88,6 @@ func Http(r chi.Router) { }) r.Route("/cert", func(r chi.Router) { - r.Use(middleware.MustLogin) cert := service.NewCertService() r.Get("/caProviders", cert.CAProviders) r.Get("/dnsProviders", cert.DNSProviders) @@ -131,7 +125,6 @@ func Http(r chi.Router) { }) r.Route("/app", func(r chi.Router) { - r.Use(middleware.MustLogin) app := service.NewAppService() r.Get("/list", app.List) r.Post("/install", app.Install) @@ -143,7 +136,6 @@ func Http(r chi.Router) { }) r.Route("/cron", func(r chi.Router) { - r.Use(middleware.MustLogin) cron := service.NewCronService() r.Get("/", cron.List) r.Post("/", cron.Create) @@ -154,7 +146,6 @@ func Http(r chi.Router) { }) r.Route("/safe", func(r chi.Router) { - r.Use(middleware.MustLogin) safe := service.NewSafeService() r.Get("/ssh", safe.GetSSH) r.Post("/ssh", safe.UpdateSSH) @@ -163,7 +154,6 @@ func Http(r chi.Router) { }) r.Route("/firewall", func(r chi.Router) { - r.Use(middleware.MustLogin) firewall := service.NewFirewallService() r.Get("/status", firewall.GetStatus) r.Post("/status", firewall.UpdateStatus) @@ -179,7 +169,6 @@ func Http(r chi.Router) { }) r.Route("/ssh", func(r chi.Router) { - r.Use(middleware.MustLogin) ssh := service.NewSSHService() r.Get("/", ssh.List) r.Post("/", ssh.Create) @@ -189,7 +178,6 @@ func Http(r chi.Router) { }) r.Route("/container", func(r chi.Router) { - r.Use(middleware.MustLogin) r.Route("/container", func(r chi.Router) { container := service.NewContainerService() r.Get("/", container.List) @@ -230,7 +218,6 @@ func Http(r chi.Router) { }) r.Route("/file", func(r chi.Router) { - r.Use(middleware.MustLogin) file := service.NewFileService() r.Post("/create", file.Create) r.Get("/content", file.Content) @@ -251,7 +238,6 @@ func Http(r chi.Router) { }) r.Route("/monitor", func(r chi.Router) { - r.Use(middleware.MustLogin) monitor := service.NewMonitorService() r.Get("/setting", monitor.GetSetting) r.Post("/setting", monitor.UpdateSetting) @@ -260,14 +246,12 @@ func Http(r chi.Router) { }) r.Route("/setting", func(r chi.Router) { - r.Use(middleware.MustLogin) setting := service.NewSettingService() r.Get("/", setting.Get) r.Post("/", setting.Update) }) r.Route("/systemctl", func(r chi.Router) { - r.Use(middleware.MustLogin) systemctl := service.NewSystemctlService() r.Get("/status", systemctl.Status) r.Get("/isEnabled", systemctl.IsEnabled) @@ -280,7 +264,6 @@ func Http(r chi.Router) { }) r.Route("/apps", func(r chi.Router) { - r.Use(middleware.MustLogin) apps.Boot(r) }) })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-fm3m-jrgm-5ppgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-53534ghsaADVISORY
- github.com/tnborg/panel/commit/4985eb2e1f388ecd6faf331941c13cb97368ec1dghsaWEB
- github.com/tnborg/panel/commit/91ecd04c270061429f9df5ec19cd6b96a9f595f2ghsaWEB
- github.com/tnborg/panel/commit/ed5c74c7534230ba685273504af4c1e1e3598ff1nvdWEB
- github.com/tnborg/panel/releases/tag/v2.5.6nvdWEB
- github.com/tnborg/panel/security/advisories/GHSA-fm3m-jrgm-5ppgnvdWEB
News mentions
0No linked articles in our index yet.