Weak password hashing with MD5

Use of a broken or risky cryptographic algorithm

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 provides golang.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.GenerateFromPassword generates a random salt and includes it in the result, while argon2.IDKey requires 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/md5 produces 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.GenerateFromPassword combines a random salt with deliberately expensive processing. bcrypt.CompareHashAndPassword verifies the password against the stored hash. Using the library increases the cost of guessing compared with MD5 without requiring custom salt or iteration handling.

References