[HOTFIX] Prevent Username Enumeration (#950)
* [HOTFIX] Prevent Username Enumeration * thanks to TheHllm for identifying the bug: https://github.com/TheHllm * temporarily prevents username enumeration with file auth * proper calculated and very slightly random fix to come * closely replicate behaviour * allow error to bubble up * Synchronize security documentation. Co-authored-by: Clement Michaud <clement.michaud34@gmail.com>pull/951/head
parent
6d8f45513f
commit
e95c6a294d
|
@ -103,8 +103,9 @@ Authelia takes security very seriously. We follow the rule of
|
||||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
||||||
encourage the community to as well.
|
encourage the community to as well.
|
||||||
|
|
||||||
Would you like to report any vulnerability discovered in Authelia, please first contact
|
|
||||||
**clems4ever** on [Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
If you discover a vulnerability in Authelia, please first contact **clems4ever** on
|
||||||
|
[Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
||||||
[email](mailto:clement.michaud34@gmail.com).
|
[email](mailto:clement.michaud34@gmail.com).
|
||||||
|
|
||||||
For details about security measures implemented in Authelia, please follow
|
For details about security measures implemented in Authelia, please follow
|
||||||
|
|
|
@ -4,8 +4,8 @@ Authelia takes security very seriously. We follow the rule of
|
||||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we
|
||||||
encourage the community to as well.
|
encourage the community to as well.
|
||||||
|
|
||||||
Would you like to report any vulnerability discovered in Authelia, please first contact
|
If you discover a vulnerability in Authelia, please first contact **clems4ever** on
|
||||||
**clems4ever** on [Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
[Matrix](https://riot.im/app/#/room/#authelia:matrix.org) or by
|
||||||
[email](mailto:clement.michaud34@gmail.com).
|
[email](mailto:clement.michaud34@gmail.com).
|
||||||
|
|
||||||
For details about security measures implemented in Authelia, please follow
|
For details about security measures implemented in Authelia, please follow
|
||||||
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/simia-tech/crypt"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/authelia/authelia/internal/configuration/schema"
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileUserProvider is a provider reading details from a file.
|
// FileUserProvider is a provider reading details from a file.
|
||||||
|
@ -18,6 +20,9 @@ type FileUserProvider struct {
|
||||||
configuration *schema.FileAuthenticationBackendConfiguration
|
configuration *schema.FileAuthenticationBackendConfiguration
|
||||||
database *DatabaseModel
|
database *DatabaseModel
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
|
|
||||||
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
||||||
|
fakeHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserDetailsModel is the model of user details in the file database.
|
// UserDetailsModel is the model of user details in the file database.
|
||||||
|
@ -46,10 +51,23 @@ func NewFileUserProvider(configuration *schema.FileAuthenticationBackendConfigur
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
||||||
|
// This generates a hash that should be usable to do a fake CheckUserPassword
|
||||||
|
algorithm := configuration.Password.Algorithm
|
||||||
|
if configuration.Password.Algorithm == "sha512" {
|
||||||
|
algorithm = HashingAlgorithmSHA512
|
||||||
|
}
|
||||||
|
settings := getCryptSettings(utils.RandomString(configuration.Password.SaltLength, HashingPossibleSaltCharacters),
|
||||||
|
algorithm, configuration.Password.Iterations, configuration.Password.Memory*1024, configuration.Password.Parallelism,
|
||||||
|
configuration.Password.KeyLength)
|
||||||
|
data := crypt.Base64Encoding.EncodeToString([]byte(utils.RandomString(configuration.Password.KeyLength, HashingPossibleSaltCharacters)))
|
||||||
|
fakeHash := fmt.Sprintf("%s$%s", settings, data)
|
||||||
|
|
||||||
return &FileUserProvider{
|
return &FileUserProvider{
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
database: database,
|
database: database,
|
||||||
lock: &sync.Mutex{},
|
lock: &sync.Mutex{},
|
||||||
|
fakeHash: fakeHash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +113,12 @@ func (p *FileUserProvider) CheckUserPassword(username string, password string) (
|
||||||
}
|
}
|
||||||
return ok, nil
|
return ok, nil
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("User '%s' does not exist in database", username)
|
|
||||||
|
// TODO: Remove this. This is only here to temporarily fix the username enumeration security flaw in #949.
|
||||||
|
hashedPassword := strings.ReplaceAll(p.fakeHash, "{CRYPT}", "")
|
||||||
|
_, err := CheckPassword(password, hashedPassword)
|
||||||
|
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDetails retrieve the groups a user belongs to.
|
// GetDetails retrieve the groups a user belongs to.
|
||||||
|
|
|
@ -68,14 +68,26 @@ func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) {
|
func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
|
||||||
WithDatabase(UserDatabaseContent, func(path string) {
|
WithDatabase(UserDatabaseContent, func(path string) {
|
||||||
config := DefaultFileAuthenticationBackendConfiguration
|
config := DefaultFileAuthenticationBackendConfiguration
|
||||||
config.Path = path
|
config.Path = path
|
||||||
provider := NewFileUserProvider(&config)
|
provider := NewFileUserProvider(&config)
|
||||||
_, err := provider.CheckUserPassword("fake", "password")
|
|
||||||
assert.Error(t, err)
|
ok, err := provider.CheckUserPassword("enumeration", "wrong_password")
|
||||||
assert.Equal(t, "User 'fake' does not exist in database", err.Error())
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
|
||||||
|
WithDatabase(UserDatabaseContent, func(path string) {
|
||||||
|
config := DefaultFileAuthenticationBackendConfiguration
|
||||||
|
config.Path = path
|
||||||
|
provider := NewFileUserProvider(&config)
|
||||||
|
ok, err := provider.CheckUserPassword("fake", "password")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, false, ok)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +269,11 @@ users:
|
||||||
james:
|
james:
|
||||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||||
email: james.dean@authelia.com
|
email: james.dean@authelia.com
|
||||||
|
|
||||||
|
|
||||||
|
enumeration:
|
||||||
|
password: "$argon2id$v=19$m=131072,p=8$BpLnfgDsc2WD8F2q$O126GHPeZ5fwj7OLSs7PndXsTbje76R+QW9/EGfhkJg"
|
||||||
|
email: james.dean@authelia.com
|
||||||
`)
|
`)
|
||||||
|
|
||||||
var MalformedUserDatabaseContent = []byte(`
|
var MalformedUserDatabaseContent = []byte(`
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnecti
|
||||||
if url.Scheme == "ldaps" {
|
if url.Scheme == "ldaps" {
|
||||||
logging.Logger().Trace("LDAP client starts a TLS session")
|
logging.Logger().Trace("LDAP client starts a TLS session")
|
||||||
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
|
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
|
||||||
InsecureSkipVerify: p.configuration.SkipVerify,
|
InsecureSkipVerify: p.configuration.SkipVerify, //nolint:gosec
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -127,11 +127,7 @@ func HashPassword(password, salt, algorithm string, iterations, memory, parallel
|
||||||
if salt == "" {
|
if salt == "" {
|
||||||
salt = utils.RandomString(saltLength, HashingPossibleSaltCharacters)
|
salt = utils.RandomString(saltLength, HashingPossibleSaltCharacters)
|
||||||
}
|
}
|
||||||
if algorithm == HashingAlgorithmArgon2id {
|
settings = getCryptSettings(salt, algorithm, iterations, memory, parallelism, keyLength)
|
||||||
settings, _ = crypt.Argon2idSettings(memory, iterations, parallelism, keyLength, salt)
|
|
||||||
} else if algorithm == HashingAlgorithmSHA512 {
|
|
||||||
settings = fmt.Sprintf("$6$rounds=%d$%s", iterations, salt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This error can be ignored because we check for it before a user gets here
|
// This error can be ignored because we check for it before a user gets here
|
||||||
hash, _ = crypt.Crypt(password, settings)
|
hash, _ = crypt.Crypt(password, settings)
|
||||||
|
@ -150,3 +146,14 @@ func CheckPassword(password, hash string) (ok bool, err error) {
|
||||||
}
|
}
|
||||||
return hash == expectedHash, nil
|
return hash == expectedHash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCryptSettings(salt, algorithm string, iterations, memory, parallelism, keyLength int) (settings string) {
|
||||||
|
if algorithm == HashingAlgorithmArgon2id {
|
||||||
|
settings, _ = crypt.Argon2idSettings(memory, iterations, parallelism, keyLength, salt)
|
||||||
|
} else if algorithm == HashingAlgorithmSHA512 {
|
||||||
|
settings = fmt.Sprintf("$6$rounds=%d$%s", iterations, salt)
|
||||||
|
} else {
|
||||||
|
panic("invalid password hashing algorithm provided")
|
||||||
|
}
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue