Cryptographic Failures Code Review Guide
Table of Contents
1. Introduction to Cryptographic Failures
Cryptographic Failures (previously known as "Sensitive Data Exposure") is ranked #2 on the OWASP Top 10. It covers any situation where cryptography is either missing, misused, or relies on weak algorithms — allowing attackers to steal credentials, financial data, health records, and other sensitive information.
Why This Matters
Unlike injection attacks that exploit input handling, cryptographic failures often hide in plain sight: a hardcoded key here, an MD5 hash there, a missing TLS check somewhere else. During code review, knowing exactly what patterns to flag can mean the difference between a secure system and a catastrophic data breach.
In this guide, you'll learn how to spot weak and deprecated algorithms (MD5, SHA-1, DES, RC4), identify insecure random number generation, find hardcoded keys, secrets, and IVs, detect improper key management and storage, catch missing encryption for data at rest and in transit, and apply these code review patterns across Python, Java, Node.js, and Go.
Common Cryptographic Failure Categories
- • MD5 / SHA-1 for hashing
- • DES / 3DES / RC4 encryption
- • ECB mode for block ciphers
- • RSA with small key sizes
- • Hardcoded secrets in code
- • Static IVs / nonces
- • Keys stored with data
- • No key rotation policy
- • HTTP instead of HTTPS
- • Plaintext PII in databases
- • Disabled TLS verification
- • Missing HSTS headers
Which OWASP Top 10 ranking does 'Cryptographic Failures' hold as of 2021?
2. Weak Hashing Algorithms
Hashing is used for password storage, data integrity verification, and digital signatures. Using a broken or weak hash algorithm means an attacker can reverse the hash, find collisions, or brute-force passwords with commodity hardware.
Hash Algorithm Security Status
| Algorithm | Output Size | Status | Known Attacks |
|---|---|---|---|
| MD5 | 128-bit | ❌ Broken | Collision attacks in seconds; rainbow tables widely available |
| SHA-1 | 160-bit | ❌ Deprecated | SHAttered attack (2017) — practical collision found by Google |
| SHA-256 | 256-bit | ✅ Secure | No known practical attacks; NIST approved |
| SHA-3 | 256/512-bit | ✅ Secure | Different construction (Keccak); quantum-resistant design |
| bcrypt | Variable | ✅ Secure (passwords) | Adaptive cost factor; salt built-in |
| Argon2 | Variable | ✅ Recommended (passwords) | Winner of Password Hashing Competition; memory-hard |
❌ Vulnerable: MD5 for Password Hashing
1import hashlib
2
3def store_password(password: str) -> str:
4 # ❌ VULNERABLE: MD5 is broken for password hashing
5 # - No salt: identical passwords produce identical hashes
6 # - Fast: GPUs can compute billions of MD5 hashes/second
7 # - Collision-prone: two different inputs can produce the same hash
8 return hashlib.md5(password.encode()).hexdigest()
9
10def verify_password(password: str, stored_hash: str) -> bool:
11 return hashlib.md5(password.encode()).hexdigest() == stored_hash✅ Secure: bcrypt / Argon2 for Password Hashing
1import bcrypt
2from argon2 import PasswordHasher
3
4# ✅ Option 1: bcrypt (widely supported, battle-tested)
5def store_password_bcrypt(password: str) -> str:
6 salt = bcrypt.gensalt(rounds=12) # Adaptive cost factor
7 return bcrypt.hashpw(password.encode(), salt).decode()
8
9def verify_password_bcrypt(password: str, stored_hash: str) -> bool:
10 return bcrypt.checkpw(password.encode(), stored_hash.encode())
11
12# ✅ Option 2: Argon2id (recommended for new projects)
13ph = PasswordHasher(
14 time_cost=3, # Number of iterations
15 memory_cost=65536, # 64 MB memory usage
16 parallelism=4 # Parallel threads
17)
18
19def store_password_argon2(password: str) -> str:
20 return ph.hash(password)
21
22def verify_password_argon2(password: str, stored_hash: str) -> bool:
23 try:
24 return ph.verify(stored_hash, password)
25 except Exception:
26 return FalseWhy is MD5 unsuitable for password hashing even with a salt added?
3. Weak Encryption Algorithms
Encryption protects confidentiality of data at rest and in transit. Using deprecated ciphers (DES, 3DES, RC4, Blowfish with small keys) or ECB mode renders the encryption effectively useless against a determined attacker.
Encryption Algorithm Security
| Algorithm | Key Size | Status | Issue |
|---|---|---|---|
| DES | 56-bit | ❌ Broken | Brute-forced in hours with modern hardware |
| 3DES | 168-bit (effective 112) | ❌ Deprecated | Sweet32 birthday attack; NIST disallowed after 2023 |
| RC4 | Variable | ❌ Broken | Statistical biases; banned in TLS (RFC 7465) |
| AES-128-ECB | 128-bit | ⚠️ Misuse | ECB mode leaks patterns — identical blocks produce identical ciphertext |
| AES-256-GCM | 256-bit | ✅ Recommended | Authenticated encryption; prevents tampering and pattern leakage |
| ChaCha20-Poly1305 | 256-bit | ✅ Recommended | Fast on devices without AES hardware; used in TLS 1.3 |
❌ Vulnerable: DES with ECB Mode
1import javax.crypto.Cipher;
2import javax.crypto.spec.SecretKeySpec;
3
4public class InsecureEncryption {
5 // ❌ VULNERABLE: Multiple issues
6 // 1. DES has only a 56-bit key — trivially brute-forced
7 // 2. ECB mode leaks patterns in the plaintext
8 // 3. No authentication — ciphertext can be tampered with silently
9 public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
10 SecretKeySpec keySpec = new SecretKeySpec(key, "DES");
11 Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
12 cipher.init(Cipher.ENCRYPT_MODE, keySpec);
13 return cipher.doFinal(data);
14 }
15}✅ Secure: AES-256-GCM with Proper IV
1import javax.crypto.Cipher;
2import javax.crypto.spec.GCMParameterSpec;
3import javax.crypto.spec.SecretKeySpec;
4import java.security.SecureRandom;
5
6public class SecureEncryption {
7 private static final int GCM_TAG_LENGTH = 128;
8 private static final int GCM_IV_LENGTH = 12;
9
10 public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
11 byte[] iv = new byte[GCM_IV_LENGTH];
12 new SecureRandom().nextBytes(iv);
13
14 SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
15 GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
16
17 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
18 cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
19
20 byte[] ciphertext = cipher.doFinal(data);
21
22 // Prepend IV to ciphertext for decryption
23 byte[] result = new byte[iv.length + ciphertext.length];
24 System.arraycopy(iv, 0, result, 0, iv.length);
25 System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
26 return result;
27 }
28}What is the main danger of using ECB (Electronic Codebook) mode?
4. Insecure Random Number Generation
Cryptographic operations require unpredictable random numbers for keys, IVs, nonces, tokens, and salts. Using a non-cryptographic PRNG (pseudo-random number generator) produces predictable output that attackers can reproduce.
❌ Vulnerable: Math.random() for Security Tokens
1// ❌ VULNERABLE: Math.random() is NOT cryptographically secure
2// It uses a linear congruential generator or xorshift — both are predictable
3function generateSessionToken() {
4 return Math.random().toString(36).substring(2);
5}
6
7function generateResetToken() {
8 // An attacker who observes a few tokens can predict future ones
9 return Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
10}✅ Secure: crypto.getRandomValues() / crypto.randomUUID()
1// ✅ SECURE: Use the Web Crypto API or Node.js crypto module
2// Browser
3function generateSessionToken() {
4 const buffer = new Uint8Array(32);
5 crypto.getRandomValues(buffer);
6 return Array.from(buffer, b => b.toString(16).padStart(2, '0')).join('');
7}
8
9// Node.js
10import { randomBytes, randomUUID } from 'node:crypto';
11
12function generateResetToken() {
13 return randomBytes(32).toString('hex'); // 256 bits of entropy
14}
15
16function generateSessionId() {
17 return randomUUID(); // v4 UUID using crypto-safe RNG
18}PRNG Security by Language
| Language | ❌ Insecure | ✅ Secure |
|---|---|---|
| JavaScript | Math.random() | crypto.getRandomValues() / crypto.randomBytes() |
| Python | random.random() | secrets.token_hex() / os.urandom() |
| Java | java.util.Random | java.security.SecureRandom |
| Go | math/rand | crypto/rand |
| C# | System.Random | System.Security.Cryptography.RandomNumberGenerator |
| Ruby | rand() | SecureRandom.hex() |
A developer uses Math.random().toString(36).slice(2) to generate password reset tokens. What is the risk?
5. Hardcoded Keys & Secrets
Hardcoded cryptographic keys, API secrets, and passwords are one of the most common findings in code review. Once a key is committed to source control, it must be considered compromised — even if the commit is later removed, it remains in the git history.
❌ Vulnerable: Hardcoded Secrets
1// ❌ VULNERABLE: Secrets embedded directly in source code
2const JWT_SECRET = "super-secret-jwt-key-2024";
3const DB_PASSWORD = "admin123!";
4const API_KEY = "sk-live-4eC39HqLyjWDarjtT1zdp7dc";
5
6// Even "obfuscated" keys are trivially reversed
7const ENCRYPTED_KEY = Buffer.from("c3VwZXItc2VjcmV0", "base64").toString();
8
9// Secrets in configuration objects
10const config = {
11 encryption: {
12 key: "0123456789abcdef0123456789abcdef",
13 iv: "abcdefghijklmnop", // ❌ Static IV defeats encryption
14 },
15 database: {
16 connectionString: "postgres://admin:P@ssw0rd@db.prod.internal:5432/app"
17 }
18};✅ Secure: Environment Variables and Secret Managers
1// ✅ SECURE: Load secrets from environment or secret manager
2const JWT_SECRET = process.env.JWT_SECRET;
3if (!JWT_SECRET || JWT_SECRET.length < 32) {
4 throw new Error("JWT_SECRET must be set and at least 32 characters");
5}
6
7// ✅ Use a secrets manager for production
8import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
9
10async function getSecret(name: string): Promise<string> {
11 const client = new SecretManagerServiceClient();
12 const [version] = await client.accessSecretVersion({
13 name: `projects/my-project/secrets/${name}/versions/latest`,
14 });
15 return version.payload?.data?.toString() || '';
16}
17
18// ✅ Generate encryption keys properly — never hardcode
19import { generateKeySync } from 'node:crypto';
20const key = generateKeySync('aes', { length: 256 });Git History Warning
Removing a secret from code does NOT remove it from git history. If a key was ever committed, rotate it immediately. Use tools like git-secrets, truffleHog, or gitleaks to scan for leaked secrets.
A developer Base64-encodes an API key before embedding it in source code. Is this secure?
9. Code Review Checklist
Use this checklist during code review to systematically identify cryptographic failures. Each item maps to a real-world vulnerability class.
Cryptographic Failures Review Checklist
1CRYPTOGRAPHIC FAILURES CODE REVIEW CHECKLIST
2
31. HASHING
4 [ ] No MD5 or SHA-1 for any security purpose
5 [ ] Passwords use bcrypt, scrypt, or Argon2id
6 [ ] Password hashing has appropriate cost/iteration params
7 [ ] HMAC used for integrity verification (not plain hash)
8
92. ENCRYPTION
10 [ ] No DES, 3DES, RC4, or Blowfish
11 [ ] AES used with GCM or CCM mode (not ECB or bare CBC)
12 [ ] Minimum 128-bit keys (256-bit preferred)
13 [ ] Unique IV/nonce per encryption operation
14 [ ] Authenticated encryption used (GCM, ChaCha20-Poly1305)
15
163. KEY MANAGEMENT
17 [ ] No hardcoded keys, passwords, or secrets in source
18 [ ] Keys loaded from environment variables or secret manager
19 [ ] Keys are not logged or included in error messages
20 [ ] Key rotation mechanism exists
21 [ ] Password-based keys use proper KDF (PBKDF2 ≥600K iters)
22
234. RANDOMNESS
24 [ ] Only CSPRNG used for security-sensitive values
25 [ ] No Math.random(), random.random(), java.util.Random
26 [ ] Sufficient entropy (≥128 bits) for tokens and keys
27
285. DATA PROTECTION
29 [ ] Sensitive data encrypted at rest (PII, credentials)
30 [ ] All external communication over HTTPS/TLS 1.2+
31 [ ] HSTS header deployed with adequate max-age
32 [ ] Cookies use Secure, HttpOnly, SameSite flags
33
346. CERTIFICATES & TLS
35 [ ] TLS certificate verification not disabled
36 [ ] SSL/TLS warnings not suppressed
37 [ ] Minimum TLS 1.2 enforced; TLS 1.3 preferred
38 [ ] Strong cipher suites onlyYou find this code in a Node.js auth module: crypto.createHash('sha1').update(password).digest('hex'). What do you flag?
10. Conclusion
Cryptographic failures are among the most impactful and yet most preventable vulnerabilities. By knowing which patterns to flag — weak hashing, deprecated ciphers, hardcoded keys, missing encryption, disabled TLS verification — you can catch these issues before they reach production.
Key Takeaways
1) Always use bcrypt/Argon2 for passwords — never MD5, SHA-1, or plain SHA-256. 2) Use AES-256-GCM or ChaCha20-Poly1305 for encryption — never DES, RC4, or ECB mode. 3) Use CSPRNG for all tokens and keys — never Math.random() or java.util.Random. 4) Never hardcode secrets — use environment variables or a secrets manager. 5) Encrypt data at rest and enforce HTTPS + HSTS for data in transit. 6) Never disable TLS certificate verification — use a custom CA bundle instead.
Essential Reading: OWASP Top 10 — A02:2021 Cryptographic Failures, NIST SP 800-131A (Transitioning Crypto Algorithms), NIST SP 800-132 (Password-Based Key Derivation), Mozilla Server Side TLS Guidelines.