# GO-CRYPTO-005: MD5 Used for Password Hashing

> **Severity:** CRITICAL | **CWE:** CWE-916, CWE-327, CWE-328 | **OWASP:** A02:2021, A07:2021

- **Language:** Go
- **Category:** Security
- **URL:** https://codepathfinder.dev/registry/golang/security/GO-CRYPTO-005
- **Detection:** `pathfinder scan --ruleset golang/GO-CRYPTO-005 --project .`

## Description

Using MD5 to hash passwords is a critical vulnerability. Password hashing requires
an intentionally slow, memory-hard function — MD5 is the opposite: it is designed
for speed and runs at **164.1 billion hashes per second** on a single NVIDIA RTX 4090
GPU (hashcat v6.2.6 benchmark).

**Cracking speed in context**:
- "8-char lowercase alphanumeric (36^8 ≈ 2.8 trillion): exhausted in ~17 seconds."
- "8-char mixed-case + digits (62^8 ≈ 218 trillion): exhausted in ~22 minutes."
- "bcrypt cost-10 on the same RTX 4090: ~184,000 hashes/second — ~892,000x slower."
- "argon2id: ~1,000–10,000 hashes/second — ~16,000,000x slower than MD5."

**Why salted SHA-256 is also insufficient**: Adding a salt stops rainbow table attacks
but does not slow the attacker on GPUs. Salted SHA-256 is still computed at billions
of hashes per second. bcrypt's work factor and argon2id's memory hardness are
specifically designed to keep per-hash computation time at 100ms–1s even on dedicated
hardware, making bulk cracking economically infeasible.

**Real breach consequences**: The "rockyou.txt" password list (32 million entries from
the 2009 RockYou breach, which stored passwords in plaintext) became the most widely
used cracking wordlist in all password cracking tools — demonstrating how breached data
permanently enables future attacks against other services where users reuse credentials.

**Detection**: This rule detects MD5 hash output flowing into password-named functions
(storePassword, checkPassword, savePassword, hashPassword, etc.), indicating password
storage using MD5.


## Vulnerable Code

```python
# --- file: vulnerable.go ---
// GO-CRYPTO-005 positive test cases — all SHOULD be detected
package main

import (
	"crypto/md5"
	"fmt"
)

// savePassword stores a hashed password — name matches *password* sink pattern
func savePassword(hash string) {
	fmt.Println("stored:", hash)
}

// storeUserPassword demonstrates MD5 output flowing into a password-named function
func storeUserPassword(username, plaintext string) {
	sum := md5.Sum([]byte(plaintext))         // SOURCE: md5 hash output
	savePassword(fmt.Sprintf("%x", sum))      // SINK: md5 result flows into password func
}

// checkPassword matches *password* sink pattern
func checkPassword(hash string) bool {
	return hash == "d8578edf8458ce06fbc5bb76a58c5ca4"
}

// loginUser — MD5 output flows into checkPassword
func loginUser(password string) bool {
	sum := md5.Sum([]byte(password))          // SOURCE: md5 hash output
	return checkPassword(fmt.Sprintf("%x", sum)) // SINK: flows into checkPassword
}

# --- file: go.mod ---
module example.com/go-crypto-005/positive

go 1.21

# --- file: go.sum ---

```

## Secure Code

```python
// SECURE: bcrypt — intentionally slow, adaptive work factor
import "golang.org/x/crypto/bcrypt"

func storePassword(username, password string) error {
    // Cost 12 recommended for 2024+; increase as server permits
    hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    if err != nil {
        return err
    }
    return db.Exec("INSERT INTO users(username, password_hash) VALUES($1, $2)",
        username, string(hash))
}

func checkPassword(storedHash, input string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(input))
    return err == nil
}

// SECURE: argon2id — PHC winner, memory-hard, recommended for new systems
import (
    "crypto/rand"
    "golang.org/x/crypto/argon2"
)

func storePasswordArgon2(password string) ([]byte, []byte, error) {
    salt := make([]byte, 16)
    if _, err := rand.Read(salt); err != nil {
        return nil, nil, err
    }
    // OWASP recommended parameters: time=1, memory=46MiB, threads=1, keyLen=32
    hash := argon2.IDKey([]byte(password), salt, 1, 46*1024, 1, 32)
    return hash, salt, nil
}

// MIGRATION: upgrade cost factor at next successful login
func loginAndUpgrade(db *sql.DB, username, password string) error {
    var storedHash string
    db.QueryRow("SELECT password_hash FROM users WHERE username=$1", username).Scan(&storedHash)
    if err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)); err != nil {
        return err
    }
    cost, _ := bcrypt.Cost([]byte(storedHash))
    if cost < 12 {
        newHash, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
        db.Exec("UPDATE users SET password_hash=$1 WHERE username=$2", string(newHash), username)
    }
    return nil
}

```

## Detection Rule (Python SDK)

```python
"""GO-CRYPTO-005: MD5 used for password hashing (critical misuse)."""

from codepathfinder.go_rule import QueryType
from codepathfinder import calls, flows
from codepathfinder.presets import PropagationPresets
from codepathfinder.go_decorators import go_rule


class GoCryptoMD5(QueryType):
    fqns = ["crypto/md5"]
    patterns = ["md5.*"]
    match_subclasses = False


@go_rule(
    id="GO-CRYPTO-005",
    severity="CRITICAL",
    cwe="CWE-327",
    owasp="A02:2021",
    tags="go,security,crypto,md5,password-hash,CWE-327,OWASP-A02",
    message=(
        "MD5 is being used to hash passwords. MD5 is completely unsuitable for "
        "password storage — MD5 hashes can be cracked in seconds using GPU-accelerated "
        "rainbow tables or dictionary attacks. "
        "Use bcrypt (golang.org/x/crypto/bcrypt), scrypt, or argon2 for password hashing."
    ),
)
def detect_md5_password_hash():
    """Detect MD5 used for password hashing."""
    return flows(
        from_sources=[
            GoCryptoMD5.method("New", "Sum"),
        ],
        to_sinks=[
            calls("*password*", "hashPassword", "storePassword", "savePassword"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Replace all MD5 password hashing with bcrypt (golang.org/x/crypto/bcrypt, cost >= 12).
- For new systems, prefer argon2id (golang.org/x/crypto/argon2) — PHC winner, memory-hard.
- For migration: at next successful login, re-hash the password with bcrypt/argon2id.
- Never use any fast hash (MD5, SHA-1, SHA-256) directly for password storage — even salted.
- bcrypt has a 72-byte input limit — enforce this or pre-hash input before passing to bcrypt.
- Use constant-time comparison (bcrypt.CompareHashAndPassword) to prevent timing attacks.

## Security Implications

- **Instant Password Cracking After Database Breach:** An attacker who exfiltrates a database of MD5-hashed passwords can crack the vast
majority in hours using a single consumer GPU. Studies of breached password datasets
consistently show 90–99% crack rates for MD5 hashes within 24–72 hours when
dictionary + rule-based attacks are applied.

- **Credential Stuffing at Scale:** Cracked passwords from one service are used to attack other services where users
reuse credentials. A single breached MD5 database provides the attacker with a tested
password list for attacks against email, banking, and other accounts.

- **Rainbow Table Vulnerability:** Unsalted MD5 password hashes are directly reversible using pre-computed rainbow tables
freely available online. Common passwords are recovered in milliseconds without
any cracking computation.


## References

- [OWASP Password Storage Cheat Sheet (bcrypt/argon2id recommendations)](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [Go golang.org/x/crypto/bcrypt package](https://pkg.go.dev/golang.org/x/crypto/bcrypt)
- [Go golang.org/x/crypto/argon2 package](https://pkg.go.dev/golang.org/x/crypto/argon2)
- [RFC 9106 — Argon2 Memory-Hard Password Hashing Function](https://www.rfc-editor.org/rfc/rfc9106)
- [bcrypt original paper: A Future-Adaptable Password Scheme (Provos & Mazières, USENIX 1999)](https://www.usenix.org/event/usenix99/provos/provos.pdf)
- [Password Hashing Competition — Argon2 winner (PHC)](https://www.password-hashing.net/)
- [NIST SP 800-63B — Digital Identity Guidelines, Authentication](https://pages.nist.gov/800-63-3/sp800-63b.html)
- [Hashcat RTX 4090 benchmark: MD5=164.1 GH/s, bcrypt=184 kH/s](https://gist.github.com/Chick3nman/32e662a5bb63bc4f51b847bb422222fd)
- [RockYou breach — Wikipedia](https://en.wikipedia.org/wiki/RockYou)
- [CWE-916: Use of Password Hash With Insufficient Computational Effort](https://cwe.mitre.org/data/definitions/916.html)
- [CWE-327: Use of a Broken or Risky Cryptographic Algorithm](https://cwe.mitre.org/data/definitions/327.html)

---

Source: https://codepathfinder.dev/registry/golang/security/GO-CRYPTO-005
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
