Description
MD5 is unsuitable for storing or verifying passwords. Its speed lets attackers test many password candidates against leaked hashes, and without a salt they can reuse precomputed tables. Recovery depends on password strength and available resources; a general collision attack does not itself recover a particular user's password.
Potential impact
- Fast offline guessing can recover passwords from leaked hashes.
- Stolen passwords may enable account takeover, access to personal data, fraudulent payments, or misuse of administrative functions.
- Reused passwords may expose accounts on other services.
- Insecure password storage may violate applicable privacy and security requirements and lead to penalties.
Remediation
- Use a password-hashing algorithm such as
bcrypt, scrypt, or Argon2. Go providesgolang.org/x/crypto/bcrypt. - Set a sufficient bcrypt cost, for example 10 or higher, and tune it with performance tests to make guessing more expensive.
- Handle salts according to the API:
bcrypt.GenerateFromPasswordgenerates a random salt and includes it in the result, whileargon2.IDKeyrequires a caller-supplied salt. Use a reviewed password-storage format for the selected algorithm. - Gradually migrate existing MD5 hashes: after a successful login, rehash the password with bcrypt or another suitable algorithm and replace the stored hash.
- Use established libraries rather than implementing hashing, salting, or password derivation yourself.
Examples
Before
go
package main
import (
"crypto/md5"
"encoding/hex"
)
type User struct {
PasswordHash string
}
// Before: hash passwords with MD5
func (u *User) SetPassword(plain string) {
h := md5.New()
h.Write([]byte(plain))
sum := h.Sum(nil)
// Storing passwords as MD5 hashes is insecure
u.PasswordHash = hex.EncodeToString(sum)
}
func (u *User) CheckPassword(plain string) bool {
h := md5.New()
h.Write([]byte(plain))
sum := h.Sum(nil)
return u.PasswordHash == hex.EncodeToString(sum)
}
After
go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
type User struct {
PasswordHash string
}
// After: hash passwords with bcrypt
func (u *User) SetPassword(plain string) error {
// Set cost to at least 10 and tune it with performance testing
hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return err
}
u.PasswordHash = string(hash)
return nil
}
func (u *User) CheckPassword(plain string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(plain))
return err == nil
}
func main() {
u := &User{}
if err := u.SetPassword("MySecurePassword!123"); err != nil {
panic(err)
}
fmt.Println("login ok?", u.CheckPassword("MySecurePassword!123")) // true
fmt.Println("login ok?", u.CheckPassword("wrong")) // false
}
Explanation:
- Before:
crypto/md5produces fast, unsalted password hashes. Attackers can test many guesses using GPUs or ASICs, and identical passwords produce identical hashes that permit reuse of precomputed tables. - After:
bcrypt.GenerateFromPasswordcombines a random salt with deliberately expensive processing.bcrypt.CompareHashAndPasswordverifies the password against the stored hash. Using the library increases the cost of guessing compared with MD5 without requiring custom salt or iteration handling.