From c22925c2fba981875d0a2b09dd3ee41c0ae4c310 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 14 Jul 2026 16:49:59 +0300 Subject: [PATCH] fix: use constant time in user checks (#1004) --- internal/controller/proxy_controller_test.go | 4 +++- internal/controller/user_controller.go | 1 + internal/controller/user_controller_test.go | 5 ++++- internal/middleware/context_middleware.go | 4 ++++ .../middleware/context_middleware_test.go | 5 ++++- internal/service/auth_service.go | 19 +++++++++++++++++-- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index faa9934b..87148109 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -708,7 +708,7 @@ func TestProxyController(t *testing.T) { Log: log, }) - authService := service.NewAuthService(service.AuthServiceInput{ + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -721,6 +721,8 @@ func TestProxyController(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + for _, test := range tests { t.Run(test.description, func(t *testing.T) { router := gin.Default() diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index ae6c23bf..00c3fbaf 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -90,6 +90,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrUserNotFound) { + controller.auth.DummyPasswordCheck(req.Password) controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt") controller.auth.RecordLoginAttempt(req.Username, false) controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found") diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index 4f081b9b..a971a5d8 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -542,7 +542,8 @@ func TestUserController(t *testing.T) { Runtime: &runtime, Ctx: ctx, }) - authService := service.NewAuthService(service.AuthServiceInput{ + + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -555,6 +556,8 @@ func TestUserController(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + beforeEach := func() { // Clear failed login attempts before each test authService.ClearLoginAttempts() diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index f49c85a1..0b31f6e2 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "errors" "fmt" "net/http" "strings" @@ -244,6 +245,9 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. search, err := m.auth.SearchUser(username) if err != nil { + if errors.Is(err, service.ErrUserNotFound) { + m.auth.DummyPasswordCheck(password) + } return nil, nil, fmt.Errorf("error searching for user: %w", err) } diff --git a/internal/middleware/context_middleware_test.go b/internal/middleware/context_middleware_test.go index 8a89e419..9a2df892 100644 --- a/internal/middleware/context_middleware_test.go +++ b/internal/middleware/context_middleware_test.go @@ -264,7 +264,8 @@ func TestContextMiddleware(t *testing.T) { Runtime: &runtime, Ctx: ctx, }) - authService := service.NewAuthService(service.AuthServiceInput{ + + authService, err := service.NewAuthService(service.AuthServiceInput{ Log: log, Config: &cfg, Runtime: &runtime, @@ -277,6 +278,8 @@ func TestContextMiddleware(t *testing.T) { PolicyEngine: policyEngine, }) + require.NoError(t, err) + contextMiddleware := NewContextMiddleware(ContextMiddlewareInput{ Log: log, RuntimeConfig: &runtime, diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index eeb5c8e1..b6d0ba56 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -69,6 +69,8 @@ type AuthService struct { tailscale *TailscaleService policyEngine *PolicyEngine + dummyHash string + lockdown struct { active bool until time.Time @@ -101,7 +103,7 @@ type AuthServiceInput struct { PolicyEngine *PolicyEngine } -func NewAuthService(i AuthServiceInput) *AuthService { +func NewAuthService(i AuthServiceInput) (*AuthService, error) { service := &AuthService{ log: i.Log, runtime: i.Runtime, @@ -123,6 +125,15 @@ func NewAuthService(i AuthServiceInput) *AuthService { loginCacheSize = service.maxLoginLimits } + // dummy hash + dummyHash, err := bcrypt.GenerateFromPassword([]byte(utils.GenerateString(8)), bcrypt.DefaultCost) + + if err != nil { + return nil, fmt.Errorf("failed to generate dummy hash: %w", err) + } + + service.dummyHash = string(dummyHash) + // caches setup oauthCache := NewCacheStore[OAuthPendingSession](256) loginCache := NewCacheStore[LoginAttempt](loginCacheSize) @@ -148,7 +159,11 @@ func NewAuthService(i AuthServiceInput) *AuthService { } }, ding.RingMinor) - return service + return service, nil +} + +func (auth *AuthService) DummyPasswordCheck(password string) { + bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(password)) } func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {