8a94a08511
Now covers all OWASP Top 10:2025 categories: - A03: supply-chain.md (SolarWinds, Bybit, npm worm examples) - A04: cryptography.md (algorithm recommendations, key management) - A08: deserialization.md (pickle, yaml, language-specific risks) - A10: error-handling.md (fail closed, error messages)
3.8 KiB
3.8 KiB
Cryptographic Failures
Rule
Use strong, modern algorithms. Never implement your own crypto. Manage keys securely.
Source: OWASP Top 10 2025 - A04 Cryptographic Failures
Algorithms to Use
| Purpose | Recommended | Avoid |
|---|---|---|
| Symmetric encryption | AES-256-GCM | DES, 3DES, RC4, ECB mode |
| Hashing (general) | SHA-256, SHA-3 | MD5, SHA-1 |
| Password hashing | bcrypt, Argon2, scrypt | SHA-*, MD5, plain hash |
| Key exchange | ECDH, X25519 | RSA < 2048 bits |
| Signatures | Ed25519, ECDSA | RSA < 2048 bits |
| TLS | 1.2+ | SSL, TLS 1.0, 1.1 |
Correct Pattern
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os
import base64
# Generate a secure key
def generate_key() -> bytes:
return Fernet.generate_key()
# Encrypt data
def encrypt(data: bytes, key: bytes) -> bytes:
f = Fernet(key)
return f.encrypt(data)
# Decrypt data
def decrypt(ciphertext: bytes, key: bytes) -> bytes:
f = Fernet(key)
return f.decrypt(ciphertext)
# Derive key from password (for encryption, not storage)
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=600000, # OWASP 2023 recommendation
)
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
# Generate secure random values
def generate_token(length: int = 32) -> str:
return base64.urlsafe_b64encode(os.urandom(length)).decode()
Incorrect Pattern
import hashlib
import random
# Wrong: MD5 for anything security-related
hash = hashlib.md5(data).hexdigest()
# Wrong: SHA-256 for passwords (no salt, too fast)
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Wrong: predictable random
token = random.randint(0, 999999) # Not cryptographically secure
# Wrong: hardcoded key
KEY = b"mysecretkey12345"
# Wrong: ECB mode (patterns visible in ciphertext)
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
# Wrong: rolling your own crypto
def my_encrypt(data, key):
return bytes(a ^ b for a, b in zip(data, cycle(key)))
Key Management
import os
# Load keys from environment or secret manager
def get_encryption_key() -> bytes:
key = os.environ.get("ENCRYPTION_KEY")
if not key:
raise RuntimeError("ENCRYPTION_KEY not set")
return base64.urlsafe_b64decode(key)
# Key rotation
class KeyManager:
def __init__(self):
self.current_key_id = os.environ["CURRENT_KEY_ID"]
self.keys = self._load_keys()
def encrypt(self, data: bytes) -> dict:
key = self.keys[self.current_key_id]
ciphertext = encrypt(data, key)
return {"key_id": self.current_key_id, "data": ciphertext}
def decrypt(self, envelope: dict) -> bytes:
key = self.keys[envelope["key_id"]]
return decrypt(envelope["data"], key)
TLS Configuration
import ssl
# Correct: modern TLS settings
def create_ssl_context() -> ssl.SSLContext:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
context.load_default_certs()
return context
# Wrong: disabling verification
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE # Never do this!
Edge Cases
- IV/nonce reuse breaks encryption security
- Timing attacks on comparison operations
- Side-channel attacks on key operations
- Key material in swap/core dumps
- Encrypted data without integrity (use AEAD)
- Insufficient entropy at startup