Skip to content
back to the map
technique
stage: code
2 min read

A04 Cryptographic Failures

Encryption is rarely broken; it is skipped, misconfigured or misused. Plain-text transport, fast password hashes and hard-coded keys are how data actually leaks.

testssl.sh / SSL LabsSemgrep crypto ruleslibsodiumage / KMS envelope encryptionHashcat (to test your own hashes)

What it is

A cryptographic failure is anything that exposes sensitive data because cryptography was missing, weak, or used incorrectly: data sent without TLS, passwords stored with a fast hash, secrets in source, predictable "random" tokens, home-made algorithms, or keys that never rotate.

It is A04 in :2025. The first question is always: which data is sensitive, where does it travel and rest, and who can read it there?

How the attack works

  • Network sniffing or a downgrade attack reads traffic that is not protected by modern TLS.
  • A database leak yields passwords hashed with MD5 or SHA-1 (or unsalted); a GPU cracks most of them in hours.
  • Tokens built from a timestamp or a non-cryptographic random generator are predicted and forged.
  • A key committed to a repository, or one shared by every customer, decrypts everything at once.
  • ECB mode, reused nonces or encryption without authentication let an attacker spot patterns or modify ciphertext.

Vulnerable code

A fast, unsalted hash for passwords and a predictable generator for a reset token.

auth.pypython
import hashlib, random

def store_password(pw):
    return hashlib.md5(pw.encode()).hexdigest()      # fast and unsalted

def reset_token():
    return str(random.randint(100000, 999999))         # predictable
auth.jsjavascript
const crypto = require("crypto");

const storePassword = (pw) => crypto.createHash("sha1").update(pw).digest("hex");
const resetToken = () => Math.random().toString(36).slice(2);
Auth.javajava
String hash = DigestUtils.md5Hex(password);
String token = String.valueOf(new Random().nextInt(999999));

The fix

Use a slow, salted, purpose-built password hash (, scrypt or bcrypt) through a maintained library, and take tokens from the operating system's cryptographic random source. Do not invent algorithms; do not manage raw keys by hand if a library or a can do it.

auth.pypython
import secrets
from argon2 import PasswordHasher

ph = PasswordHasher()

def store_password(pw):
    return ph.hash(pw)                    # salt and parameters are included

def verify_password(stored, pw):
    try:
        return ph.verify(stored, pw)
    except Exception:
        return False

def reset_token():
    return secrets.token_urlsafe(32)      # 256 bits from the OS CSPRNG
auth.jsjavascript
const crypto = require("crypto");
const argon2 = require("argon2");

const storePassword = (pw) => argon2.hash(pw, { type: argon2.argon2id });
const verifyPassword = (stored, pw) => argon2.verify(stored, pw);
const resetToken = () => crypto.randomBytes(32).toString("base64url");
Auth.java (Spring Security)java
PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String hash = encoder.encode(password);
boolean ok = encoder.matches(password, hash);

byte[] raw = new byte[32];
new SecureRandom().nextBytes(raw);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
token.gogo
func ResetToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil { // crypto/rand
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(b), nil
}

Prove the fix

Test properties, not algorithms: two hashes of the same password differ (salted), the stored value is not a bare fast hash, tokens are long and unique, and the transport enforces modern TLS.

test_crypto.py (pytest)python
def test_hashes_are_salted_and_slow_format():
    a, b = store_password("correct horse"), store_password("correct horse")
    assert a != b                          # random salt per hash
    assert a.startswith("$argon2id$")       # not md5/sha1 hex
    assert verify_password(a, "correct horse")
    assert not verify_password(a, "wrong")

def test_reset_tokens_are_long_and_unique():
    tokens = {reset_token() for _ in range(1000)}
    assert len(tokens) == 1000
    assert all(len(t) >= 43 for t in tokens)   # 32 bytes, base64url
tls-check.shbash
# fail if the server still accepts legacy protocols
testssl.sh --protocols --quiet https://staging.example.com | tee tls.txt
! grep -E 'TLS 1(\.0|\.1)? .*offered' tls.txt

Checklist

  • Classify data: know what is sensitive before choosing controls.
  • TLS everywhere, modern versions only, HSTS on; no sensitive data in URLs.
  • Passwords: Argon2id, scrypt or bcrypt with a maintained library; never a fast hash.
  • Randomness for security comes from the OS , never from Math.random or random.
  • Use (AES-GCM, ChaCha20-Poly1305); never reuse a nonce with the same key.
  • Keys live in a KMS or secret manager, are rotated, and are never in the repository.
  • Do not store what you do not need; delete it when you are done.
OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →Argon2idA memory-hard password hashing function with a built-in salt. Slowness is the point: it makes each guess expensive for an attacker.see in glossary →KMSkey management serviceA managed service that creates, stores and controls the use of cryptographic keys. The application rarely touches the master key: it asks the KMS to encrypt or decrypt, and the provider records every use in an auditable log. Without a KMS, the key ends up on disk and becomes a target.see in glossary →CSPRNGcryptographically secure random number generatorThe operating system's source of unpredictable bytes (secrets in Python, crypto.randomBytes in Node, crypto/rand in Go). Use it for tokens and keys, never Math.random.see in glossary →Authenticated encryptionAEADEncryption that also detects tampering (AES-GCM, ChaCha20-Poly1305). Encryption alone does not stop an attacker from modifying ciphertext.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

Study before

Unlocks

test what you learned

Loading questions…