From 8a94a0851160882470283a77dc662a1326b82761 Mon Sep 17 00:00:00 2001 From: Rodin Date: Sun, 10 May 2026 22:48:39 -0700 Subject: [PATCH] Add supply-chain, deserialization, cryptography, error-handling patterns 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) --- README.md | 58 +++++++++++---- cryptography.md | 140 ++++++++++++++++++++++++++++++++++ deserialization.md | 151 +++++++++++++++++++++++++++++++++++++ error-handling.md | 182 +++++++++++++++++++++++++++++++++++++++++++++ supply-chain.md | 126 +++++++++++++++++++++++++++++++ 5 files changed, 641 insertions(+), 16 deletions(-) create mode 100644 cryptography.md create mode 100644 deserialization.md create mode 100644 error-handling.md create mode 100644 supply-chain.md diff --git a/README.md b/README.md index 888fb5c..badd266 100644 --- a/README.md +++ b/README.md @@ -6,36 +6,62 @@ Scannable patterns for security code review. Each file has: - **Incorrect Pattern** — common mistakes - **Edge Cases** — gotchas +Based on OWASP Top 10:2025 and recent security research. + ## Patterns ### Fundamentals -| File | Topic | -|------|-------| -| [secure-defaults.md](secure-defaults.md) | Fail closed, deny by default, defense in depth | -| [input-validation.md](input-validation.md) | Allowlist > blocklist, validate at boundaries | -| [credential-handling.md](credential-handling.md) | No hardcoded secrets, environment/secret manager | -| [audit-logging.md](audit-logging.md) | What to log, what not to log | +| File | Topic | OWASP 2025 | +|------|-------|------------| +| [secure-defaults.md](secure-defaults.md) | Fail closed, deny by default, defense in depth | A06 | +| [input-validation.md](input-validation.md) | Allowlist > blocklist, validate at boundaries | A03 | +| [credential-handling.md](credential-handling.md) | No hardcoded secrets, environment/secret manager | — | +| [audit-logging.md](audit-logging.md) | What to log, what not to log | A09 | +| [error-handling.md](error-handling.md) | Fail closed, no sensitive info in errors | A10 | ### Identity -| File | Topic | -|------|-------| -| [authentication.md](authentication.md) | Passwords, tokens, MFA, brute force protection | -| [authorization.md](authorization.md) | Permission checks, IDOR prevention, privilege escalation | +| File | Topic | OWASP 2025 | +|------|-------|------------| +| [authentication.md](authentication.md) | Passwords, tokens, MFA, brute force protection | A07 | +| [authorization.md](authorization.md) | Permission checks, IDOR prevention, privilege escalation | A01 | ### Attack Prevention -| File | Topic | -|------|-------| -| [injection-prevention.md](injection-prevention.md) | SQL, command, template, path traversal | -| [dos-prevention.md](dos-prevention.md) | Rate limiting, resource bounds, algorithmic complexity | -| [prompt-injection.md](prompt-injection.md) | LLM security, data/instruction separation | +| File | Topic | OWASP 2025 | +|------|-------|------------| +| [injection-prevention.md](injection-prevention.md) | SQL, command, template, path traversal | A05 | +| [dos-prevention.md](dos-prevention.md) | Rate limiting, resource bounds, algorithmic complexity | — | +| [prompt-injection.md](prompt-injection.md) | LLM security, data/instruction separation | — | +| [deserialization.md](deserialization.md) | Untrusted data deserialization, pickle, yaml | A08 | + +### Infrastructure + +| File | Topic | OWASP 2025 | +|------|-------|------------| +| [supply-chain.md](supply-chain.md) | SBOM, dependency scanning, signed packages | A03 | +| [cryptography.md](cryptography.md) | Strong algorithms, key management, TLS | A04 | + +## OWASP Top 10:2025 Coverage + +| # | Category | Pattern | +|---|----------|---------| +| A01 | Broken Access Control | authorization.md | +| A02 | Security Misconfiguration | secure-defaults.md | +| A03 | Software Supply Chain Failures | supply-chain.md | +| A04 | Cryptographic Failures | cryptography.md | +| A05 | Injection | injection-prevention.md | +| A06 | Insecure Design | secure-defaults.md | +| A07 | Authentication Failures | authentication.md | +| A08 | Software or Data Integrity Failures | deserialization.md | +| A09 | Security Logging and Alerting Failures | audit-logging.md | +| A10 | Mishandling of Exceptional Conditions | error-handling.md | ## Sources +- [OWASP Top 10:2025](https://owasp.org/Top10/2025/) - [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/) -- [OWASP Top 10](https://owasp.org/Top10/) - [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/) - [CWE (Common Weakness Enumeration)](https://cwe.mitre.org/) diff --git a/cryptography.md b/cryptography.md new file mode 100644 index 0000000..2bfdff9 --- /dev/null +++ b/cryptography.md @@ -0,0 +1,140 @@ +# Cryptographic Failures + +## Rule + +Use strong, modern algorithms. Never implement your own crypto. Manage keys securely. + +**Source:** [OWASP Top 10 2025 - A04 Cryptographic Failures](https://owasp.org/Top10/2025/A04_2025-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 + +```python +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 + +```python +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 + +```python +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 + +```python +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 diff --git a/deserialization.md b/deserialization.md new file mode 100644 index 0000000..8ed9ddb --- /dev/null +++ b/deserialization.md @@ -0,0 +1,151 @@ +# Insecure Deserialization + +## Rule + +Never deserialize untrusted data without validation. Prefer data-only formats. + +**Source:** [OWASP Top 10 2025 - A08 Software or Data Integrity Failures](https://owasp.org/Top10/2025/A08_2025-Software_or_Data_Integrity_Failures/) + +## Why It's Dangerous + +Deserialization can: +- Execute arbitrary code +- Instantiate arbitrary objects +- Bypass authentication +- Cause denial of service + +## Correct Pattern + +```python +import json +from dataclasses import dataclass +from typing import Any + +# Prefer data-only formats (JSON, not pickle) +def safe_deserialize(data: str) -> dict: + """Deserialize JSON (data-only, no code execution).""" + return json.loads(data) + +# Validate structure after deserialization +@dataclass +class UserInput: + name: str + email: str + age: int + +def parse_user_input(raw: str) -> UserInput: + data = json.loads(raw) + + # Validate required fields + if not isinstance(data.get("name"), str): + raise ValueError("Invalid name") + if not isinstance(data.get("email"), str): + raise ValueError("Invalid email") + if not isinstance(data.get("age"), int): + raise ValueError("Invalid age") + + return UserInput( + name=data["name"], + email=data["email"], + age=data["age"] + ) + +# If you must use object serialization, allowlist classes +ALLOWED_CLASSES = {"User", "Order", "Product"} + +def safe_unpickle(data: bytes, allowed: set[str]) -> Any: + """Restricted unpickler that only allows specific classes.""" + import pickle + import io + + class RestrictedUnpickler(pickle.Unpickler): + def find_class(self, module, name): + if name not in allowed: + raise pickle.UnpicklingError(f"Class {name} not allowed") + return super().find_class(module, name) + + return RestrictedUnpickler(io.BytesIO(data)).load() +``` + +## Incorrect Pattern + +```python +import pickle +import yaml + +# Wrong: pickle from untrusted source +def load_session(cookie_value: bytes): + return pickle.loads(cookie_value) # RCE! + +# Wrong: yaml.load (can execute code) +def load_config(yaml_string: str): + return yaml.load(yaml_string) # Should be yaml.safe_load + +# Wrong: eval/exec on user data +def parse_expression(expr: str): + return eval(expr) # Arbitrary code execution + +# Wrong: deserializing without validation +def process_request(data: bytes): + obj = pickle.loads(data) + obj.execute() # No type checking! +``` + +## Language-Specific Risks + +| Language | Dangerous | Safe Alternative | +|----------|-----------|------------------| +| Python | `pickle.loads()` | JSON, restricted unpickler | +| Java | `ObjectInputStream` | JSON, allowlisted classes | +| PHP | `unserialize()` | `json_decode()` | +| Ruby | `Marshal.load()` | JSON, YAML.safe_load | +| JavaScript | `eval(JSON)` | `JSON.parse()` | +| .NET | `BinaryFormatter` | `JsonSerializer` | + +## YAML Specific + +```python +import yaml + +# Wrong: yaml.load allows arbitrary Python objects +data = yaml.load(untrusted_yaml) # Can execute code! +# Attack: "!!python/object/apply:os.system ['rm -rf /']" + +# Correct: yaml.safe_load only allows basic types +data = yaml.safe_load(untrusted_yaml) +``` + +## Signature Verification + +If you must accept serialized objects: + +```python +import hmac +import hashlib + +SECRET_KEY = get_secret("serialization_key") + +def sign_data(data: bytes) -> bytes: + """Sign serialized data.""" + signature = hmac.new(SECRET_KEY, data, hashlib.sha256).digest() + return signature + data + +def verify_and_load(signed_data: bytes) -> Any: + """Verify signature before deserializing.""" + signature = signed_data[:32] + data = signed_data[32:] + + expected = hmac.new(SECRET_KEY, data, hashlib.sha256).digest() + if not hmac.compare_digest(signature, expected): + raise SecurityError("Invalid signature") + + return restricted_deserialize(data) +``` + +## Edge Cases + +- Base64-encoded serialized data in cookies +- Serialized objects in database fields +- Message queues with serialized payloads +- Session data in Redis/Memcached +- Java RMI (Remote Method Invocation) diff --git a/error-handling.md b/error-handling.md new file mode 100644 index 0000000..3ab6976 --- /dev/null +++ b/error-handling.md @@ -0,0 +1,182 @@ +# Error Handling + +## Rule + +Handle all errors explicitly. Fail closed. Never leak sensitive information in error messages. + +**Source:** [OWASP Top 10 2025 - A10 Mishandling of Exceptional Conditions](https://owasp.org/Top10/2025/A10_2025-Mishandling_of_Exceptional_Conditions/) + +## Fail Closed vs Fail Open + +| Scenario | Fail Closed (Correct) | Fail Open (Wrong) | +|----------|----------------------|-------------------| +| Auth check errors | Deny access | Allow access | +| Input validation errors | Reject request | Process anyway | +| Transaction errors | Roll back | Partial commit | +| Permission check timeout | Deny | Allow | + +## Correct Pattern + +```python +import logging +from contextlib import contextmanager + +# Explicit error handling with fail-closed +def check_permission(user_id: str, resource_id: str) -> bool: + """Return False on any error — fail closed.""" + try: + permissions = fetch_permissions(user_id) + return resource_id in permissions.allowed_resources + except Exception as e: + logging.exception("Permission check failed", extra={ + "user_id": user_id, + "resource_id": resource_id + }) + return False # Deny on error + +# Transaction rollback on failure +@contextmanager +def transaction(): + """Ensure complete rollback on any failure.""" + tx = begin_transaction() + try: + yield tx + tx.commit() + except Exception: + tx.rollback() + raise + +def transfer_funds(from_acct: str, to_acct: str, amount: Decimal): + with transaction() as tx: + debit(tx, from_acct, amount) + credit(tx, to_acct, amount) + # If credit fails, debit is rolled back + +# Generic error messages to users +def handle_request(request): + try: + return process(request) + except ValidationError as e: + # Specific, safe error for user + return {"error": str(e)}, 400 + except Exception as e: + # Log details internally + logging.exception("Unexpected error", extra={ + "request_id": request.id + }) + # Generic message to user + return {"error": "An unexpected error occurred"}, 500 +``` + +## Incorrect Pattern + +```python +# Wrong: fail open +def check_access(user_id, resource): + try: + return has_permission(user_id, resource) + except: + return True # "If in doubt, let them in" + +# Wrong: swallowing exceptions +try: + process_payment() +except: + pass # Silently fails, state unknown + +# Wrong: leaking sensitive info +except DatabaseError as e: + return {"error": f"Database error: {e}"} # Exposes internals + +# Wrong: stack trace to user +except Exception as e: + import traceback + return {"error": traceback.format_exc()} + +# Wrong: partial transaction +def transfer(from_acct, to_acct, amount): + debit(from_acct, amount) + try: + credit(to_acct, amount) + except: + pass # Debit happened but credit didn't! +``` + +## Error Message Guidelines + +| Internal Log | User-Facing Message | +|--------------|---------------------| +| `SQLException: column 'password' at line 5` | `An error occurred. Please try again.` | +| `FileNotFoundError: /etc/shadow` | `Resource not found.` | +| `ConnectionError: redis://prod-cache:6379` | `Service temporarily unavailable.` | +| `KeyError: user['admin_token']` | `Invalid request.` | + +## Global Exception Handler + +```python +from flask import Flask, jsonify +import logging + +app = Flask(__name__) + +@app.errorhandler(Exception) +def handle_exception(e): + """Global handler — catch anything we missed.""" + # Log full details + logging.exception("Unhandled exception") + + # Return generic error to user + if app.debug: + # Only in dev — never in prod + return {"error": str(e)}, 500 + else: + return {"error": "Internal server error"}, 500 + +# Rate limit repeated errors (DOS prevention) +class ErrorRateLimiter: + def __init__(self, max_errors: int = 100, window: int = 60): + self.max_errors = max_errors + self.window = window + self.errors = [] + + def record_error(self, error_type: str): + now = time.time() + self.errors = [t for t in self.errors if now - t < self.window] + self.errors.append(now) + + if len(self.errors) > self.max_errors: + logging.warning(f"Error rate limit exceeded: {error_type}") + # Could trigger alerting or blocking +``` + +## Unchecked Return Values + +```python +# Wrong: ignoring return values +def process_file(path): + f = open(path) # Could fail + data = f.read() + f.close() + return data + +# Correct: handle all failure modes +def process_file(path: str) -> str: + try: + with open(path) as f: + return f.read() + except FileNotFoundError: + raise ValueError(f"File not found: {path}") + except PermissionError: + raise ValueError(f"Permission denied: {path}") + except IOError as e: + raise ValueError(f"IO error reading file: {e}") +``` + +## Edge Cases + +- Errors during error handling (recursive failure) +- Resource leaks when exceptions occur +- Timeout handling (treat as failure) +- Async error handling (unhandled promise rejections) +- Background job failures (need monitoring) +- Partial failures in distributed systems diff --git a/supply-chain.md b/supply-chain.md new file mode 100644 index 0000000..e6ab923 --- /dev/null +++ b/supply-chain.md @@ -0,0 +1,126 @@ +# Supply Chain Security + +## Rule + +Verify integrity of all dependencies. Generate SBOMs. Monitor for vulnerabilities. + +**Source:** [OWASP Top 10 2025 - A03 Software Supply Chain Failures](https://owasp.org/Top10/2025/A03_2025-Software_Supply_Chain_Failures/) + +## Attack Examples + +- **SolarWinds (2019)**: Compromised build system, 18,000 orgs affected +- **Bybit (2025)**: Supply chain attack in wallet software, $1.5B theft +- **Shai-Hulud (2025)**: Self-propagating npm worm, 500+ packages + +## Correct Pattern + +```python +# Generate and maintain SBOM +import subprocess +import json +import hashlib + +def generate_sbom(project_path: str) -> dict: + """Generate Software Bill of Materials.""" + # Use CycloneDX or SPDX format + result = subprocess.run( + ["cyclonedx-py", "poetry", "-o", "sbom.json"], + cwd=project_path, + capture_output=True + ) + with open(f"{project_path}/sbom.json") as f: + return json.load(f) + +# Verify package integrity +def verify_package(package_path: str, expected_hash: str) -> bool: + """Verify package hash before installation.""" + with open(package_path, "rb") as f: + actual_hash = hashlib.sha256(f.read()).hexdigest() + return actual_hash == expected_hash + +# Pin dependencies with hashes +# requirements.txt with hashes: +# requests==2.28.0 --hash=sha256:abc123... + +# Lock file example (poetry.lock, package-lock.json) +def verify_lockfile_integrity(lockfile_path: str) -> bool: + """Ensure lockfile hasn't been tampered with.""" + # Compare against known-good version in version control + ... +``` + +## Incorrect Pattern + +```python +# Wrong: no version pinning +# requirements.txt +# requests +# flask + +# Wrong: pulling from arbitrary sources +pip install https://sketchy-site.com/package.tar.gz + +# Wrong: no integrity verification +def install_dependency(name): + os.system(f"pip install {name}") # No hash check + +# Wrong: auto-updating without verification +def auto_update(): + os.system("pip install --upgrade -r requirements.txt") +``` + +## Dependency Scanning + +```python +# Integrate vulnerability scanning in CI +def scan_dependencies() -> list[dict]: + """Scan for known vulnerabilities.""" + # Use tools like: + # - OWASP Dependency-Check + # - Snyk + # - GitHub Dependabot + # - OSV (Open Source Vulnerabilities) + + result = subprocess.run( + ["pip-audit", "--format=json"], + capture_output=True + ) + return json.loads(result.stdout) + +def block_on_critical(vulnerabilities: list[dict]) -> bool: + """Fail CI on critical vulnerabilities.""" + critical = [v for v in vulnerabilities if v["severity"] == "CRITICAL"] + if critical: + raise SecurityError(f"Critical vulnerabilities found: {critical}") + return True +``` + +## CI/CD Hardening + +```python +# Verify CI/CD pipeline integrity +PIPELINE_REQUIREMENTS = { + "mfa_required": True, + "branch_protection": True, + "signed_commits": True, + "code_review_required": True, + "secrets_scanning": True, +} + +def audit_pipeline(config: dict) -> list[str]: + """Audit CI/CD configuration.""" + issues = [] + for requirement, expected in PIPELINE_REQUIREMENTS.items(): + if config.get(requirement) != expected: + issues.append(f"Missing: {requirement}") + return issues +``` + +## Edge Cases + +- Transitive dependencies (deps of deps) can be vulnerable +- Typosquatting attacks (similar package names) +- Dependency confusion (internal vs public package names) +- Compromised maintainer accounts +- Post-install scripts can execute arbitrary code +- IDE extensions and dev tools are part of supply chain