From 6e256254e6127d0b4a6e160ef5421b07048e5994 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 14 Jul 2026 16:05:15 +0300 Subject: [PATCH 1/3] fix: use constant time in user checks --- internal/controller/user_controller.go | 1 + internal/middleware/context_middleware.go | 1 + internal/service/auth_service.go | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index ae6c23bf5..7c5597cb0 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() 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/middleware/context_middleware.go b/internal/middleware/context_middleware.go index f49c85a17..af344aa81 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -244,6 +244,7 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. search, err := m.auth.SearchUser(username) if err != nil { + m.auth.DummyPasswordCheck() return nil, nil, fmt.Errorf("error searching for user: %w", err) } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index eeb5c8e17..61ed47a1f 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,12 @@ func NewAuthService(i AuthServiceInput) *AuthService { } }, ding.RingMinor) - return service + return service, nil +} + +func (auth *AuthService) DummyPasswordCheck() { + random := utils.GenerateString(8) + bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(random)) } func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) { From d2418ac16ee9195f3b1bf0259eb5021b928d3a67 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 14 Jul 2026 16:16:51 +0300 Subject: [PATCH 2/3] fix: rabbit comments --- internal/controller/proxy_controller_test.go | 4 +++- internal/controller/user_controller_test.go | 5 ++++- internal/middleware/context_middleware.go | 5 ++++- internal/middleware/context_middleware_test.go | 5 ++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index faa9934b4..871481098 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_test.go b/internal/controller/user_controller_test.go index 4f081b9b0..a971a5d82 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 af344aa81..aa144428b 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,7 +245,9 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. search, err := m.auth.SearchUser(username) if err != nil { - m.auth.DummyPasswordCheck() + if errors.Is(err, service.ErrUserNotFound) { + m.auth.DummyPasswordCheck() + } 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 8a89e4190..9a2df8920 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, From 5d2b7c976ce0f3c9fda5570b20aa0c09500ee92b Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 14 Jul 2026 16:35:04 +0300 Subject: [PATCH 3/3] fix: more rabbit comments --- internal/controller/user_controller.go | 2 +- internal/middleware/context_middleware.go | 2 +- internal/service/auth_service.go | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 7c5597cb0..00c3fbaf4 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -90,7 +90,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { if err != nil { if errors.Is(err, service.ErrUserNotFound) { - controller.auth.DummyPasswordCheck() + 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/middleware/context_middleware.go b/internal/middleware/context_middleware.go index aa144428b..0b31f6e2e 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -246,7 +246,7 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. if err != nil { if errors.Is(err, service.ErrUserNotFound) { - m.auth.DummyPasswordCheck() + m.auth.DummyPasswordCheck(password) } return nil, nil, fmt.Errorf("error searching for user: %w", err) } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 61ed47a1f..b6d0ba56c 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -162,9 +162,8 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) { return service, nil } -func (auth *AuthService) DummyPasswordCheck() { - random := utils.GenerateString(8) - bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(random)) +func (auth *AuthService) DummyPasswordCheck(password string) { + bcrypt.CompareHashAndPassword([]byte(auth.dummyHash), []byte(password)) } func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) {