VYPR
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.

PackageAffected versionsPatched versions
github.com/1Panel-dev/1PanelGo
< 2.0.142.0.14
github.com/1Panel-dev/1Panel/coreGo
< 0.0.0-20251128030527-ac43f00273be0.0.0-20251128030527-ac43f00273be

Affected products

1

Patches

1
ac43f00273be

perf: optimize login API logic (#11104)

https://github.com/1Panel-dev/1PanelCityFunNov 28, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.