High severityNVD Advisory· Published Dec 9, 2025· Updated Dec 9, 2025
1Panel – CAPTCHA Bypass via Client-Controlled Flag
CVE-2025-66507
Description
1Panel is an open-source, web-based control panel for Linux server management. Versions 2.0.13 and below allow an unauthenticated attacker to disable CAPTCHA verification by abusing a client-controlled parameter. Because the server previously trusted this value without proper validation, CAPTCHA protections can be bypassed, enabling automated login attempts and significantly increasing the risk of account takeover (ATO). This issue is fixed in version 2.0.14.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/1Panel-dev/1PanelGo | < 2.0.14 | 2.0.14 |
github.com/1Panel-dev/1Panel/coreGo | < 0.0.0-20251128030527-ac43f00273be | 0.0.0-20251128030527-ac43f00273be |
Affected products
1- Range: < 2.0.14
Patches
1ac43f00273beperf: optimize login API logic (#11104)
9 files changed · +143 −28
core/app/api/v2/auth.go+21 −9 modified@@ -2,6 +2,7 @@ package v2 import ( "encoding/base64" + "github.com/1Panel-dev/1Panel/core/utils/common" "os" "path" @@ -29,12 +30,15 @@ func (b *BaseApi) Login(c *gin.Context) { return } - if !req.IgnoreCaptcha { + ip := common.GetRealClientIP(c) + needCaptcha := global.IPTracker.NeedCaptcha(ip) + if needCaptcha { if errMsg := captcha.VerifyCode(req.CaptchaID, req.Captcha); errMsg != "" { helper.BadAuth(c, errMsg, nil) return } } + entranceItem := c.Request.Header.Get("EntranceCode") var entrance []byte if len(entranceItem) != 0 { @@ -50,13 +54,18 @@ func (b *BaseApi) Login(c *gin.Context) { user, msgKey, err := authService.Login(c, req, string(entrance)) go saveLoginLogs(c, err) if msgKey == "ErrAuth" || msgKey == "ErrEntrance" { + if msgKey == "ErrAuth" { + global.IPTracker.SetNeedCaptcha(ip) + } helper.BadAuth(c, msgKey, err) return } if err != nil { + global.IPTracker.SetNeedCaptcha(ip) helper.InternalServer(c, err) return } + global.IPTracker.Clear(ip) helper.SuccessWithData(c, user) } @@ -142,15 +151,18 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) { helper.InternalServer(c, err) return } + ip := common.GetRealClientIP(c) + needCaptcha := global.IPTracker.NeedCaptcha(ip) res := &dto.LoginSetting{ - IsDemo: global.CONF.Base.IsDemo, - IsIntl: global.CONF.Base.IsIntl, - IsFxplay: global.CONF.Base.IsFxplay, - IsOffLine: global.CONF.Base.IsOffLine, - Language: settingInfo.Language, - MenuTabs: settingInfo.MenuTabs, - PanelName: settingInfo.PanelName, - Theme: settingInfo.Theme, + IsDemo: global.CONF.Base.IsDemo, + IsIntl: global.CONF.Base.IsIntl, + IsFxplay: global.CONF.Base.IsFxplay, + IsOffLine: global.CONF.Base.IsOffLine, + Language: settingInfo.Language, + MenuTabs: settingInfo.MenuTabs, + PanelName: settingInfo.PanelName, + Theme: settingInfo.Theme, + NeedCaptcha: needCaptcha, } helper.SuccessWithData(c, res) }
core/app/dto/auth.go+5 −6 modified@@ -23,12 +23,11 @@ type MfaCredential struct { } type Login struct { - Name string `json:"name" validate:"required"` - Password string `json:"password" validate:"required"` - IgnoreCaptcha bool `json:"ignoreCaptcha"` - Captcha string `json:"captcha"` - CaptchaID string `json:"captchaID"` - Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"` + Name string `json:"name" validate:"required"` + Password string `json:"password" validate:"required"` + Captcha string `json:"captcha"` + CaptchaID string `json:"captchaID"` + Language string `json:"language" validate:"required,oneof=zh en 'zh-Hant' ko ja ru ms 'pt-BR' tr 'es-ES'"` } type MFALogin struct {
core/app/dto/setting.go+9 −8 modified@@ -241,12 +241,13 @@ type AppstoreConfig struct { } type LoginSetting struct { - IsDemo bool `json:"isDemo"` - IsIntl bool `json:"isIntl"` - IsOffLine bool `json:"isOffLine"` - IsFxplay bool `json:"isFxplay"` - Language string `json:"language"` - MenuTabs string `json:"menuTabs"` - PanelName string `json:"panelName"` - Theme string `json:"theme"` + IsDemo bool `json:"isDemo"` + IsIntl bool `json:"isIntl"` + IsOffLine bool `json:"isOffLine"` + IsFxplay bool `json:"isFxplay"` + Language string `json:"language"` + MenuTabs string `json:"menuTabs"` + PanelName string `json:"panelName"` + Theme string `json:"theme"` + NeedCaptcha bool `json:"needCaptcha"` }
core/app/service/auth.go+1 −2 modified@@ -3,8 +3,6 @@ package service import ( "crypto/hmac" "encoding/base64" - "strconv" - "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/buserr" @@ -13,6 +11,7 @@ import ( "github.com/1Panel-dev/1Panel/core/utils/encrypt" "github.com/1Panel-dev/1Panel/core/utils/mfa" "github.com/gin-gonic/gin" + "strconv" ) type AuthService struct{}
core/global/global.go+3 −0 modified@@ -1,6 +1,7 @@ package global import ( + "github.com/1Panel-dev/1Panel/core/init/auth" "github.com/1Panel-dev/1Panel/core/init/session/psession" "github.com/go-playground/validator/v10" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -28,6 +29,8 @@ var ( Cron *cron.Cron ScriptSyncJobID cron.EntryID + + IPTracker *auth.IPTracker ) type DBOption func(*gorm.DB) *gorm.DB
core/init/auth/ip_tracker.go+99 −0 added@@ -0,0 +1,99 @@ +package auth + +import ( + "sync" + "time" +) + +const ( + MaxIPCount = 100 + ExpireDuration = 30 * time.Minute +) + +type IPRecord struct { + NeedCaptcha bool + LastUpdate time.Time +} + +type IPTracker struct { + records map[string]*IPRecord + ipOrder []string + mu sync.RWMutex +} + +func NewIPTracker() *IPTracker { + return &IPTracker{ + records: make(map[string]*IPRecord), + ipOrder: make([]string, 0), + } +} + +func (t *IPTracker) NeedCaptcha(ip string) bool { + t.mu.Lock() + defer t.mu.Unlock() + + record, exists := t.records[ip] + if !exists { + return false + } + + if time.Since(record.LastUpdate) > ExpireDuration { + t.removeIPUnsafe(ip) + return false + } + + return record.NeedCaptcha +} + +func (t *IPTracker) SetNeedCaptcha(ip string) { + t.mu.Lock() + defer t.mu.Unlock() + + if record, exists := t.records[ip]; exists { + if time.Since(record.LastUpdate) > ExpireDuration { + t.removeIPUnsafe(ip) + } else { + record.NeedCaptcha = true + record.LastUpdate = time.Now() + return + } + } + + if len(t.records) >= MaxIPCount { + t.removeOldestUnsafe() + } + + t.records[ip] = &IPRecord{ + NeedCaptcha: true, + LastUpdate: time.Now(), + } + t.ipOrder = append(t.ipOrder, ip) +} + +func (t *IPTracker) Clear(ip string) { + t.mu.Lock() + defer t.mu.Unlock() + + t.removeIPUnsafe(ip) +} + +func (t *IPTracker) removeIPUnsafe(ip string) { + delete(t.records, ip) + + for i, storedIP := range t.ipOrder { + if storedIP == ip { + t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...) + break + } + } +} + +func (t *IPTracker) removeOldestUnsafe() { + if len(t.ipOrder) == 0 { + return + } + + oldestIP := t.ipOrder[0] + delete(t.records, oldestIP) + t.ipOrder = t.ipOrder[1:] +}
core/server/server.go+3 −0 modified@@ -4,6 +4,7 @@ import ( "crypto/tls" "encoding/gob" "fmt" + "github.com/1Panel-dev/1Panel/core/init/auth" "net" "net/http" "os" @@ -54,6 +55,8 @@ func Start() { gin.SetMode(gin.ReleaseMode) } + global.IPTracker = auth.NewIPTracker() + tcpItem := "tcp4" if global.CONF.Conn.Ipv6 == constant.StatusEnable { tcpItem = "tcp"
frontend/src/api/interface/auth.ts+1 −1 modified@@ -2,7 +2,6 @@ export namespace Login { export interface ReqLoginForm { name: string; password: string; - ignoreCaptcha: boolean; captcha: string; captchaID: string; authMethod: string; @@ -36,5 +35,6 @@ export namespace Login { panelName: string; theme: string; isOffLine: boolean; + needCaptcha: boolean; } }
frontend/src/views/login/components/login-form.vue+1 −2 modified@@ -220,7 +220,6 @@ const loginFormRef = ref<FormInstance>(); const loginForm = reactive({ name: '', password: '', - ignoreCaptcha: true, captcha: '', captchaID: '', authMethod: 'session', @@ -318,7 +317,6 @@ const login = (formEl: FormInstance | undefined) => { let requestLoginForm = { name: loginForm.name, password: encryptPassword(loginForm.password), - ignoreCaptcha: globalStore.ignoreCaptcha, captcha: loginForm.captcha, captchaID: captcha.captchaID, authMethod: 'session', @@ -418,6 +416,7 @@ const getSetting = async () => { isFxplay.value = res.data.isFxplay; globalStore.isFxplay = isFxplay.value; globalStore.isOffLine = res.data.isOffLine; + globalStore.ignoreCaptcha = !res.data.needCaptcha; document.title = res.data.panelName; i18n.warnHtmlMessage = false;
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
5- github.com/advisories/GHSA-qmg5-v42x-qqhqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66507ghsaADVISORY
- github.com/1Panel-dev/1Panel/commit/ac43f00273be745f8d04b90b6e2b9c1a40ef7bcaghsax_refsource_MISCWEB
- github.com/1Panel-dev/1Panel/releases/tag/v2.0.14ghsax_refsource_MISCWEB
- github.com/1Panel-dev/1Panel/security/advisories/GHSA-qmg5-v42x-qqhqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.