VYPR
Moderate severityNVD Advisory· Published Aug 9, 2025· Updated Aug 11, 2025

OpenBao LDAP MFA Enforcement Bypass When Using Username As Alias

CVE-2025-55001

Description

OpenBao exists to provide a software solution to manage, store, and distribute sensitive data including secrets, certificates, and keys. In versions 2.3.1 and below, OpenBao allowed the assignment of policies and MFA attribution based upon entity aliases, chosen by the underlying auth method. When the username_as_alias=true parameter in the LDAP auth method was in use, the caller-supplied username was used verbatim without normalization, allowing an attacker to bypass alias-specific MFA requirements. This issue was fixed in version 2.3.2. To work around this, remove all usage of the username_as_alias=true parameter and update any entity aliases accordingly.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/openbao/openbaoGo
>= 0.1.0, < 2.3.22.3.2
github.com/openbao/openbaoGo
< 0.0.0-20250807212521-c52795c1ef740.0.0-20250807212521-c52795c1ef74

Affected products

1

Patches

1
c52795c1ef74

Address login plugin lookahead contract failures (#1632)

https://github.com/openbao/openbaoAlexander ScheelAug 7, 2025via ghsa
13 files changed · +163 80
  • builtin/credential/jwt/path_cel_login.go+3 3 modified
    @@ -159,7 +159,7 @@ func (b *jwtAuthBackend) pathCelLogin(ctx context.Context, req *logical.Request,
     	}
     
     	// execute celRoleEntry.AuthProgram
    -	pbAuth, err := b.runCelProgram(ctx, celRoleEntry, allClaims)
    +	pbAuth, err := b.runCelProgram(ctx, req.Operation, celRoleEntry, allClaims)
     	if err != nil {
     		return logical.ErrorResponse("error executing cel program: %s", err.Error()), nil
     	}
    @@ -179,8 +179,8 @@ func (b *jwtAuthBackend) pathCelLogin(ctx context.Context, req *logical.Request,
     }
     
     // runCelProgram executes the CelProgram for the celRoleEntry and returns a pb.Auth or error
    -func (b *jwtAuthBackend) runCelProgram(ctx context.Context, celRoleEntry *celRoleEntry, allClaims map[string]any) (*pb.Auth, error) {
    -	result, err := b.celEvalProgram(celRoleEntry.CelProgram, allClaims)
    +func (b *jwtAuthBackend) runCelProgram(ctx context.Context, operation logical.Operation, celRoleEntry *celRoleEntry, allClaims map[string]any) (*pb.Auth, error) {
    +	result, err := b.celEvalProgram(celRoleEntry.CelProgram, operation, allClaims)
     	if err != nil {
     		return nil, fmt.Errorf("Cel role auth program failed: %w", err)
     	}
    
  • builtin/credential/jwt/path_cel_login_test.go+1 1 modified
    @@ -173,7 +173,7 @@ func Test_runCelProgram(t *testing.T) {
     			if !ok {
     				t.Fatalf("Expected jwtAuthBackend, got %T", logicalBackend)
     			}
    -			role, err := b.runCelProgram(context.Background(), &tc.celRole, tc.claims)
    +			role, err := b.runCelProgram(context.Background(), logical.UpdateOperation, &tc.celRole, tc.claims)
     			if tc.validateResult != nil {
     				tc.validateResult(t, err, role)
     			}
    
  • builtin/credential/jwt/path_cel_role.go+17 16 modified
    @@ -99,20 +99,20 @@ func pathCelRole(b *jwtAuthBackend) *framework.Path {
     		},
     		"expiration_leeway": {
     			Type: framework.TypeSignedDurationSecond,
    -			Description: `Duration in seconds of leeway when validating expiration of a token to account for clock skew. 
    -Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
    +			Description: `Duration in seconds of leeway when validating expiration of a token to account for clock skew.
    + Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
     			Default: claimDefaultLeeway,
     		},
     		"not_before_leeway": {
     			Type: framework.TypeSignedDurationSecond,
    -			Description: `Duration in seconds of leeway when validating not before values of a token to account for clock skew. 
    -Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
    +			Description: `Duration in seconds of leeway when validating not before values of a token to account for clock skew.
    + Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
     			Default: claimDefaultLeeway,
     		},
     		"clock_skew_leeway": {
     			Type: framework.TypeSignedDurationSecond,
    -			Description: `Duration in seconds of leeway when validating all claims to account for clock skew. 
    -Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
    +			Description: `Duration in seconds of leeway when validating all claims to account for clock skew.
    + Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
     			Default: jwt.DefaultLeeway,
     		},
     		"bound_audiences": {
    @@ -148,20 +148,20 @@ Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
     			},
     			"expiration_leeway": {
     				Type: framework.TypeSignedDurationSecond,
    -				Description: `Duration in seconds of leeway when validating expiration of a token to account for clock skew. 
    -Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
    +				Description: `Duration in seconds of leeway when validating expiration of a token to account for clock skew.
    + Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
     				Default: claimDefaultLeeway,
     			},
     			"not_before_leeway": {
     				Type: framework.TypeSignedDurationSecond,
    -				Description: `Duration in seconds of leeway when validating not before values of a token to account for clock skew. 
    -Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
    +				Description: `Duration in seconds of leeway when validating not before values of a token to account for clock skew.
    + Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
     				Default: claimDefaultLeeway,
     			},
     			"clock_skew_leeway": {
     				Type: framework.TypeSignedDurationSecond,
    -				Description: `Duration in seconds of leeway when validating all claims to account for clock skew. 
    -Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
    +				Description: `Duration in seconds of leeway when validating all claims to account for clock skew.
    + Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
     				Default: jwt.DefaultLeeway,
     			},
     			"bound_audiences": {
    @@ -413,14 +413,14 @@ func validateCelRoleCreation(b *jwtAuthBackend, entry *celRoleEntry, ctx context
     
     func (b *jwtAuthBackend) validateCelProgram(program celhelper.CelProgram) (bool, error) {
     	// adding a minimal jwtClaims collection here, for validating usages in CEL expression
    -	_, err := b.celEvalProgram(program, map[string]any{"sub": "email@example.com", "aud": "audience", "iss": "issuer"})
    +	_, err := b.celEvalProgram(program, logical.UpdateOperation, map[string]any{"sub": "email@example.com", "aud": "audience", "iss": "issuer"})
     	if err != nil {
     		return false, fmt.Errorf("failed to validate CEL program: %w", err)
     	}
     	return true, nil
     }
     
    -func (b *jwtAuthBackend) celEvalProgram(program celhelper.CelProgram, jwtClaims map[string]any) (any, error) {
    +func (b *jwtAuthBackend) celEvalProgram(program celhelper.CelProgram, operation logical.Operation, jwtClaims map[string]any) (any, error) {
     	env, err := b.celEnv(program)
     	if err != nil {
     		return nil, err
    @@ -429,8 +429,9 @@ func (b *jwtAuthBackend) celEvalProgram(program celhelper.CelProgram, jwtClaims
     	// The "request" key allows CEL expressions to access and evaluate against input fields.
     	// Additional variables and evaluated results will be added dynamically during processing.
     	evaluationData := map[string]interface{}{
    -		"claims": jwtClaims,
    -		"now":    time.Now(),
    +		"claims":    jwtClaims,
    +		"now":       time.Now(),
    +		"operation": string(operation),
     	}
     
     	// Evaluate all variables
    
  • builtin/credential/ldap/backend_test.go+63 14 modified
    @@ -136,6 +136,53 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) {
     
     	ctx := context.Background()
     
    +	// testLoginNormalized helps to validate that HCSEC-2025-16 (CVE-2025-6004)
    +	// as applicable to LDAP and HCSEC-2025-20 (CVE-2025-6013) are both
    +	// remediated in the respective configurations. Notably, the user lockout
    +	// lookahead alias needs not be the same as the final alias returned by
    +	// the login.
    +	testLoginNormalized := func() {
    +		loginReq := &logical.Request{
    +			Operation: logical.UpdateOperation,
    +			Path:      "login/Hermes Conrad",
    +			Data: map[string]interface{}{
    +				"password": "hermes",
    +			},
    +			Storage:    storage,
    +			Connection: &logical.Connection{},
    +		}
    +		resp, err = b.HandleRequest(ctx, loginReq)
    +		if err != nil || (resp != nil && resp.IsError()) {
    +			t.Fatalf("err:%v resp:%#v", err, resp)
    +		}
    +		expected := []string{"grouppolicy", "userpolicy"}
    +		if !reflect.DeepEqual(expected, resp.Auth.Policies) {
    +			t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies)
    +		}
    +
    +		// Redo the operation with a trailing space and ensure alias is
    +		// correctly normalized by the server.
    +		loginReq = &logical.Request{
    +			Operation: logical.UpdateOperation,
    +			Path:      "login/Hermes Conrad ",
    +			Data: map[string]interface{}{
    +				"password": "hermes",
    +			},
    +			Storage:    storage,
    +			Connection: &logical.Connection{},
    +		}
    +		spaceResp, err := b.HandleRequest(ctx, loginReq)
    +		if err != nil || (resp != nil && resp.IsError()) {
    +			t.Fatalf("err:%v resp:%#v", err, resp)
    +		}
    +		if !reflect.DeepEqual(expected, resp.Auth.Policies) {
    +			t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies)
    +		}
    +		if !reflect.DeepEqual(resp, spaceResp) {
    +			t.Fatalf("bad: expected same response:\n\tresp: %#v\n\tspace resp: %#v", resp, spaceResp)
    +		}
    +	}
    +
     	testVals := func(caseSensitive bool) {
     		// Clear storage
     		userList, err := storage.List(ctx, "user/")
    @@ -249,23 +296,25 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) {
     			}
     		}
     
    -		loginReq := &logical.Request{
    -			Operation: logical.UpdateOperation,
    -			Path:      "login/Hermes Conrad",
    -			Data: map[string]interface{}{
    -				"password": "hermes",
    -			},
    -			Storage:    storage,
    -			Connection: &logical.Connection{},
    +		testLoginNormalized()
    +
    +		// Adjust the configuration use username as aliases and redo the
    +		// above normalization check.
    +		configEntry, err := b.Config(ctx, configReq)
    +		if err != nil {
    +			t.Fatal(err)
     		}
    -		resp, err = b.HandleRequest(ctx, loginReq)
    -		if err != nil || (resp != nil && resp.IsError()) {
    -			t.Fatalf("err:%v resp:%#v", err, resp)
    +		configEntry.UsernameAsAlias = true
    +		entry, err := logical.StorageEntryJSON("config", configEntry)
    +		if err != nil {
    +			t.Fatal(err)
     		}
    -		expected := []string{"grouppolicy", "userpolicy"}
    -		if !reflect.DeepEqual(expected, resp.Auth.Policies) {
    -			t.Fatalf("bad: policies: expected: %q, actual: %q", expected, resp.Auth.Policies)
    +		err = configReq.Storage.Put(ctx, entry)
    +		if err != nil {
    +			t.Fatal(err)
     		}
    +
    +		testLoginNormalized()
     	}
     
     	cleanup, cfg := ldap.PrepareTestContainer(t, "latest")
    
  • builtin/credential/ldap/path_login.go+30 0 modified
    @@ -7,6 +7,7 @@ import (
     	"context"
     	"errors"
     	"fmt"
    +	"strings"
     
     	"github.com/openbao/openbao/sdk/v2/framework"
     	"github.com/openbao/openbao/sdk/v2/helper/cidrutil"
    @@ -55,6 +56,28 @@ func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Requ
     		return nil, errors.New("missing username")
     	}
     
    +	// _Some_ LDAP backends will silently trim spaces, leading to an
    +	// authentication lockout bypass if not adjusted in the alias.
    +	// We normalize the username to avoid this, as _presumably_ users
    +	// do not normally begin/end with leading space.
    +	//
    +	// See HCSEC-2025-16 / CVE-2025-6004 for more information.
    +	username = strings.TrimSpace(username)
    +
    +	cfg, err := b.Config(ctx, req)
    +	if err != nil {
    +		return nil, err
    +	}
    +	if cfg == nil {
    +		return logical.ErrorResponse("auth method not configured"), nil
    +	}
    +
    +	// Likewise, if the configuration uses a lower-case username, set that
    +	// as our alias.
    +	if cfg.CaseSensitiveNames != nil && !*cfg.CaseSensitiveNames {
    +		username = strings.ToLower(username)
    +	}
    +
     	return &logical.Response{
     		Auth: &logical.Auth{
     			Alias: &logical.Alias{
    @@ -87,6 +110,13 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framew
     	username := d.Get("username").(string)
     	password := d.Get("password").(string)
     
    +	// See notes in pathLoginAliasLookahead(...) for more information. This
    +	// is a hack specifically for HCSEC-2025-20 / CVE-2025-6013.
    +	username = strings.TrimSpace(username)
    +	if cfg.CaseSensitiveNames != nil && !*cfg.CaseSensitiveNames {
    +		username = strings.ToLower(username)
    +	}
    +
     	effectiveUsername, policies, resp, groupNames, err := b.Login(ctx, req, username, password, cfg.UsernameAsAlias)
     	if err != nil || (resp != nil && resp.IsError()) {
     		return resp, err
    
  • builtin/credential/radius/path_login.go+4 1 modified
    @@ -65,7 +65,10 @@ func pathLogin(b *backend) *framework.Path {
     func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
     	username := d.Get("username").(string)
     	if username == "" {
    -		return nil, errors.New("missing username")
    +		username = d.Get("urlusername").(string)
    +		if username == "" {
    +			return nil, errors.New("missing username")
    +		}
     	}
     
     	return &logical.Response{
    
  • builtin/credential/userpass/path_login.go+1 1 modified
    @@ -59,7 +59,7 @@ func pathLogin(b *backend) *framework.Path {
     }
     
     func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
    -	username := d.Get("username").(string)
    +	username := strings.ToLower(d.Get("username").(string))
     	if username == "" {
     		return nil, errors.New("missing username")
     	}
    
  • changelog/1632.txt+5 0 added
    @@ -0,0 +1,5 @@
    +```release-note:security
    +core/auth: Correctly handle alias lookahead for user lockout consistency. HCSEC-2025-16 / CVE-2025-6004.
    +auth/userpass: Consistently handle alias lookahead as case insensitive. HCSEC-2025-16 / CVE-2025-6004.
    +auth/ldap: Attempt consistent entity aliasing w.r.t. spacing and casing. HCSEC-2025-16 / CVE-2025-6004 and HCSEC-2025-20 / CVE-2025-6013.
    +```
    
  • vault/core.go+1 0 modified
    @@ -3294,6 +3294,7 @@ func (c *Core) loadLoginMFAConfigs(ctx context.Context) error {
     
     type MFACachedAuthResponse struct {
     	CachedAuth            *logical.Auth
    +	CachedUserLockout     *FailedLoginUser
     	RequestPath           string
     	RequestNSID           string
     	RequestNSPath         string
    
  • vault/expiration_test.go+2 2 modified
    @@ -1225,13 +1225,13 @@ func TestExpiration_RegisterAuth_NoTTL(t *testing.T) {
     	}
     
     	// First on core
    -	_, err = c.RegisterAuth(ctx, 0, "auth/github/login", auth, "", true)
    +	_, err = c.RegisterAuth(ctx, 0, "auth/github/login", auth, "", true, nil)
     	if err != nil {
     		t.Fatal(err)
     	}
     
     	auth.TokenPolicies[0] = "default"
    -	_, err = c.RegisterAuth(ctx, 0, "auth/github/login", auth, "", true)
    +	_, err = c.RegisterAuth(ctx, 0, "auth/github/login", auth, "", true, nil)
     	if err == nil {
     		t.Fatal("expected error")
     	}
    
  • vault/external_tests/userpass_binary/ip_token_binding_test.go+7 1 modified
    @@ -135,7 +135,10 @@ func Test_StrictIPBinding(t *testing.T) {
     
     		"-H", "Content-Type: application/json",
     		"--data", `{"password": "password"}`,
    -		"https://" + vaultAddr + ":8200/v1/auth/userpass/login/testing",
    +		// We switch the username to Testing to ensure case validation
    +		// does not affect user lockout attribution. This is a test
    +		// to validate our fix for HCSEC-2025-16 / CVE-2025-6004.
    +		"https://" + vaultAddr + ":8200/v1/auth/userpass/login/Testing",
     	}
     	stdout, stderr, retcode, err = curlRunner.RunCmdWithOutput(ctx, curlResult.Container.ID, curlCmd)
     	t.Logf("cURL Command: %v\nstdout: %v\nstderr: %v\n", curlCmd, string(stdout), string(stderr))
    @@ -145,8 +148,11 @@ func Test_StrictIPBinding(t *testing.T) {
     	var data map[string]interface{}
     	err = json.Unmarshal(stdout, &data)
     	require.NoError(t, err)
    +	require.NotContains(t, data, "errors")
    +	require.Contains(t, data, "auth")
     
     	auth := data["auth"].(map[string]interface{})
    +	require.Contains(t, auth, "client_token")
     	remoteToken := auth["client_token"].(string)
     
     	// Using the remote token locally should fail...
    
  • vault/login_mfa.go+3 3 modified
    @@ -797,7 +797,7 @@ func (b *LoginMFABackend) handleMFALoginValidate(ctx context.Context, req *logic
     	}
     
     	// MFA validation has passed. Let's generate the token
    -	resp, err := b.Core.LoginMFACreateToken(ctx, cachedResponseAuth.RequestPath, cachedResponseAuth.CachedAuth, req.Data, !req.IsInlineAuth)
    +	resp, err := b.Core.LoginMFACreateToken(ctx, cachedResponseAuth.RequestPath, cachedResponseAuth.CachedAuth, req.Data, !req.IsInlineAuth, cachedResponseAuth.CachedUserLockout)
     	if err != nil {
     		return nil, fmt.Errorf("failed to create a token. error: %v", err)
     	}
    @@ -822,7 +822,7 @@ func (c *Core) teardownLoginMFA() error {
     
     // LoginMFACreateToken creates a token after the login MFA is validated.
     // It also applies the lease quotas on the original login request path.
    -func (c *Core) LoginMFACreateToken(ctx context.Context, reqPath string, cachedAuth *logical.Auth, loginRequestData map[string]interface{}, persistToken bool) (*logical.Response, error) {
    +func (c *Core) LoginMFACreateToken(ctx context.Context, reqPath string, cachedAuth *logical.Auth, loginRequestData map[string]interface{}, persistToken bool, userLockoutInfo *FailedLoginUser) (*logical.Response, error) {
     	auth := cachedAuth
     	resp := &logical.Response{
     		Auth: auth,
    @@ -841,7 +841,7 @@ func (c *Core) LoginMFACreateToken(ctx context.Context, reqPath string, cachedAu
     		role = reqRole.(string)
     	}
     
    -	_, resp, err = c.LoginCreateToken(ctx, ns, reqPath, mountPoint, role, resp, persistToken)
    +	_, resp, err = c.LoginCreateToken(ctx, ns, reqPath, mountPoint, role, resp, persistToken, userLockoutInfo)
     	return resp, err
     }
     
    
  • vault/request_handling.go+26 38 modified
    @@ -1526,14 +1526,16 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     	}
     
     	// if user lockout feature is not disabled, check if the user is locked
    +	var userLockoutInfo *FailedLoginUser
     	if !isUserLockoutDisabled {
    -		isloginUserLocked, err := c.isUserLocked(ctx, entry, req)
    +		lockoutInfo, isloginUserLocked, err := c.isUserLocked(ctx, entry, req)
     		if err != nil {
     			return nil, nil, err
     		}
     		if isloginUserLocked {
     			return nil, nil, logical.ErrPermissionDenied
     		}
    +		userLockoutInfo = lockoutInfo
     	}
     
     	// Route the request
    @@ -1542,7 +1544,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     	// if routeErr has invalid credentials error, update the userFailedLoginMap
     	if routeErr != nil && routeErr == logical.ErrInvalidCredentials {
     		if !isUserLockoutDisabled {
    -			err := c.failedUserLoginProcess(ctx, entry, req)
    +			err := c.failedUserLoginProcess(ctx, entry, req, userLockoutInfo)
     			if err != nil {
     				return nil, nil, err
     			}
    @@ -1738,6 +1740,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     				// and return MFARequirement only
     				respAuth := &MFACachedAuthResponse{
     					CachedAuth:            resp.Auth,
    +					CachedUserLockout:     userLockoutInfo,
     					RequestPath:           req.Path,
     					RequestNSID:           ns.ID,
     					RequestNSPath:         ns.Path,
    @@ -1777,7 +1780,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     			role = c.DetermineRoleFromLoginRequest(ctx, req.MountPoint, req.Data)
     		}
     
    -		_, respTokenCreate, errCreateToken := c.LoginCreateToken(ctx, ns, req.Path, source, role, resp, req.IsInlineAuth)
    +		_, respTokenCreate, errCreateToken := c.LoginCreateToken(ctx, ns, req.Path, source, role, resp, req.IsInlineAuth, userLockoutInfo)
     		if errCreateToken != nil {
     			return respTokenCreate, nil, errCreateToken
     		}
    @@ -1790,16 +1793,11 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     	// For service tokens on ent it is taken care by registerAuth RPC calls.
     	// This update is done as part of registerAuth of RPC calls from standby
     	// to active node. This is added there to reduce RPC calls
    -	if !isUserLockoutDisabled && (auth.TokenType == logical.TokenTypeBatch) {
    -		loginUserInfoKey := FailedLoginUser{
    -			aliasName:     auth.Alias.Name,
    -			mountAccessor: auth.Alias.MountAccessor,
    -		}
    -
    +	if !isUserLockoutDisabled && (auth.TokenType == logical.TokenTypeBatch) && userLockoutInfo != nil {
     		// We don't need to try to delete the lockedUsers storage entry, since we're
     		// processing a login request. If a login attempt is allowed, it means the user is
     		// unlocked and we only add storage entry when the user gets locked.
    -		err = c.LocalUpdateUserFailedLoginInfo(ctx, loginUserInfoKey, nil, true)
    +		err = c.LocalUpdateUserFailedLoginInfo(ctx, *userLockoutInfo, nil, true)
     		if err != nil {
     			return nil, nil, err
     		}
    @@ -1823,7 +1821,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
     // LoginCreateToken creates a token as a result of a login request.
     // If MFA is enforced, mfa/validate endpoint calls this functions
     // after successful MFA validation to generate the token.
    -func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, reqPath, mountPoint, role string, resp *logical.Response, isInlineAuth bool) (bool, *logical.Response, error) {
    +func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, reqPath, mountPoint, role string, resp *logical.Response, isInlineAuth bool, userLockoutInfo *FailedLoginUser) (bool, *logical.Response, error) {
     	auth := resp.Auth
     	source := strings.TrimPrefix(mountPoint, credentialRoutePrefix)
     	source = strings.ReplaceAll(source, "/", "-")
    @@ -1872,7 +1870,7 @@ func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, re
     	}
     
     	leaseGenerated := false
    -	te, err := c.RegisterAuth(ctx, tokenTTL, reqPath, auth, role, !isInlineAuth)
    +	te, err := c.RegisterAuth(ctx, tokenTTL, reqPath, auth, role, !isInlineAuth, userLockoutInfo)
     	switch {
     	case err == nil:
     		if auth.TokenType != logical.TokenTypeBatch {
    @@ -1915,18 +1913,12 @@ func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, re
     // failedUserLoginProcess updates the userFailedLoginMap with login count and  last failed
     // login time for users with failed login attempt
     // If the user gets locked for current login attempt, it updates the storage entry too
    -func (c *Core) failedUserLoginProcess(ctx context.Context, mountEntry *MountEntry, req *logical.Request) error {
    +func (c *Core) failedUserLoginProcess(ctx context.Context, mountEntry *MountEntry, req *logical.Request, userLockoutInfo *FailedLoginUser) error {
     	// get the user lockout configuration for the user
     	userLockoutConfiguration := c.getUserLockoutConfiguration(mountEntry)
     
    -	// determine the key for userFailedLoginInfo map
    -	loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req)
    -	if err != nil {
    -		return err
    -	}
    -
     	// get entry from userFailedLoginInfo map for the key
    -	userFailedLoginInfo := c.LocalGetUserFailedLoginInfo(ctx, loginUserInfoKey)
    +	userFailedLoginInfo := c.LocalGetUserFailedLoginInfo(ctx, *userLockoutInfo)
     
     	// update the last failed login time with current time
     	failedLoginInfo := FailedLoginInfo{
    @@ -1949,7 +1941,7 @@ func (c *Core) failedUserLoginProcess(ctx context.Context, mountEntry *MountEntr
     	}
     
     	// update the userFailedLoginInfo map (and/or storage) with the updated/new entry
    -	err = c.LocalUpdateUserFailedLoginInfo(ctx, loginUserInfoKey, &failedLoginInfo, false)
    +	err := c.LocalUpdateUserFailedLoginInfo(ctx, *userLockoutInfo, &failedLoginInfo, false)
     	if err != nil {
     		return err
     	}
    @@ -2011,11 +2003,11 @@ func (c *Core) isUserLockoutDisabled(mountEntry *MountEntry) (bool, error) {
     }
     
     // isUserLocked determines if the login request user is locked
    -func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (locked bool, err error) {
    +func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (loginUser *FailedLoginUser, locked bool, err error) {
     	// get userFailedLoginInfo map key for login user
     	loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req)
     	if err != nil {
    -		return false, err
    +		return nil, false, err
     	}
     
     	// get entry from userFailedLoginInfo map for the key
    @@ -2028,30 +2020,30 @@ func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *lo
     		// entry not found in userFailedLoginInfo map, check storage to re-verify
     		ns, err := namespace.FromContext(ctx)
     		if err != nil {
    -			return false, fmt.Errorf("could not retrieve namespace from context: %w", err)
    +			return nil, false, fmt.Errorf("could not retrieve namespace from context: %w", err)
     		}
     
     		view := NamespaceView(c.barrier, ns).SubView(coreLockedUsersPath).SubView(loginUserInfoKey.mountAccessor + "/")
     		existingEntry, err := view.Get(ctx, loginUserInfoKey.aliasName)
     		if err != nil {
    -			return false, err
    +			return nil, false, err
     		}
     
     		var lastLoginTime int
     		if existingEntry == nil {
     			// no storage entry found, user is not locked
    -			return false, nil
    +			return &loginUserInfoKey, false, nil
     		}
     
     		err = jsonutil.DecodeJSON(existingEntry.Value, &lastLoginTime)
     		if err != nil {
    -			return false, err
    +			return nil, false, err
     		}
     
     		// if time passed from last login time is within lockout duration, the user is locked
     		if time.Now().Unix()-int64(lastLoginTime) < int64(userLockoutConfiguration.LockoutDuration.Seconds()) {
     			// user locked
    -			return true, nil
    +			return &loginUserInfoKey, true, nil
     		}
     
     		// else user is not locked. Entry is stale, this will be removed from storage during cleanup
    @@ -2064,10 +2056,11 @@ func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *lo
     
     		if isCountOverLockoutThreshold && isWithinLockoutDuration {
     			// user locked
    -			return true, nil
    +			return &loginUserInfoKey, true, nil
     		}
     	}
    -	return false, nil
    +
    +	return &loginUserInfoKey, false, nil
     }
     
     // getUserLockoutConfiguration gets the user lockout configuration for a mount entry
    @@ -2179,7 +2172,7 @@ func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*
     // store, and registers a corresponding token lease to the expiration manager.
     // role is the login role used as part of the creation of the token entry. If not
     // relevant, can be omitted (by being provided as "").
    -func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path string, auth *logical.Auth, role string, persistToken bool) (*logical.TokenEntry, error) {
    +func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path string, auth *logical.Auth, role string, persistToken bool, userLockoutInfo *FailedLoginUser) (*logical.TokenEntry, error) {
     	// We first assign token policies to what was returned from the backend
     	// via auth.Policies. Then, we get the full set of policies into
     	// auth.Policies from the backend + entity information -- this is not
    @@ -2242,16 +2235,11 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st
     		// Successful login, remove any entry from userFailedLoginInfo map
     		// if it exists. This is done for service tokens (for oss) here.
     		// For ent it is taken care by registerAuth RPC calls.
    -		if auth.Alias != nil {
    -			loginUserInfoKey := FailedLoginUser{
    -				aliasName:     auth.Alias.Name,
    -				mountAccessor: auth.Alias.MountAccessor,
    -			}
    -
    +		if userLockoutInfo != nil {
     			// We don't need to try to delete the lockedUsers storage entry, since we're
     			// processing a login request. If a login attempt is allowed, it means the user is
     			// unlocked and we only add storage entry when the user gets locked.
    -			err = c.LocalUpdateUserFailedLoginInfo(ctx, loginUserInfoKey, nil, true)
    +			err = c.LocalUpdateUserFailedLoginInfo(ctx, *userLockoutInfo, nil, true)
     			if err != nil {
     				return nil, err
     			}
    

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

6

News mentions

0

No linked articles in our index yet.