Initial commit: 9 security patterns for code review
Fundamentals: secure-defaults, input-validation, credential-handling, audit-logging Identity: authentication, authorization Attack Prevention: injection-prevention, dos-prevention, prompt-injection
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# Authentication
|
||||
|
||||
## Rule
|
||||
|
||||
Verify identity before granting access. Use proven libraries, not DIY crypto.
|
||||
|
||||
**Source:** [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||
|
||||
## Password Handling
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```python
|
||||
import bcrypt
|
||||
import secrets
|
||||
|
||||
def hash_password(password: str) -> bytes:
|
||||
"""Hash password using bcrypt with automatic salt."""
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||
|
||||
def verify_password(password: str, hashed: bytes) -> bool:
|
||||
"""Verify password against hash. Constant-time comparison."""
|
||||
return bcrypt.checkpw(password.encode(), hashed)
|
||||
|
||||
# Password requirements
|
||||
MIN_PASSWORD_LENGTH = 12
|
||||
COMMON_PASSWORDS = load_common_passwords() # Top 10k list
|
||||
|
||||
def validate_password(password: str) -> list[str]:
|
||||
"""Return list of validation errors."""
|
||||
errors = []
|
||||
if len(password) < MIN_PASSWORD_LENGTH:
|
||||
errors.append(f"Password must be at least {MIN_PASSWORD_LENGTH} characters")
|
||||
if password.lower() in COMMON_PASSWORDS:
|
||||
errors.append("Password is too common")
|
||||
return errors
|
||||
```
|
||||
|
||||
### Incorrect Pattern
|
||||
|
||||
```python
|
||||
# Wrong: plain text storage
|
||||
user.password = password
|
||||
|
||||
# Wrong: weak hashing
|
||||
user.password = hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
# Wrong: SHA without salt
|
||||
user.password = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# Wrong: reversible encryption
|
||||
user.password = encrypt(password, key)
|
||||
|
||||
# Wrong: timing attack vulnerable
|
||||
if user.password == submitted_password:
|
||||
grant_access()
|
||||
```
|
||||
|
||||
## Token Management
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```python
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def generate_token() -> str:
|
||||
"""Generate cryptographically secure token."""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
def generate_session(user_id: str) -> dict:
|
||||
"""Create session with expiration."""
|
||||
return {
|
||||
"token": generate_token(),
|
||||
"user_id": user_id,
|
||||
"created_at": datetime.utcnow(),
|
||||
"expires_at": datetime.utcnow() + timedelta(hours=24),
|
||||
}
|
||||
|
||||
def validate_session(session: dict) -> bool:
|
||||
"""Check session validity."""
|
||||
if datetime.utcnow() > session["expires_at"]:
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
### Incorrect Pattern
|
||||
|
||||
```python
|
||||
# Wrong: predictable tokens
|
||||
token = f"session_{user_id}_{int(time.time())}"
|
||||
|
||||
# Wrong: no expiration
|
||||
session = {"token": token, "user_id": user_id}
|
||||
|
||||
# Wrong: client-controlled expiration
|
||||
if request.cookies.get("expires") > now: # User can modify!
|
||||
grant_access()
|
||||
```
|
||||
|
||||
## Multi-Factor Authentication
|
||||
|
||||
```python
|
||||
import pyotp
|
||||
|
||||
def setup_totp(user_id: str) -> str:
|
||||
"""Generate TOTP secret for user."""
|
||||
secret = pyotp.random_base32()
|
||||
store_totp_secret(user_id, secret)
|
||||
return secret
|
||||
|
||||
def verify_totp(user_id: str, code: str) -> bool:
|
||||
"""Verify TOTP code with time window."""
|
||||
secret = get_totp_secret(user_id)
|
||||
totp = pyotp.TOTP(secret)
|
||||
return totp.verify(code, valid_window=1) # ±30 seconds
|
||||
```
|
||||
|
||||
## Brute Force Protection
|
||||
|
||||
```python
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
class LoginRateLimiter:
|
||||
def __init__(self):
|
||||
self.attempts = defaultdict(list)
|
||||
self.lockouts = {}
|
||||
|
||||
def record_attempt(self, identifier: str, success: bool):
|
||||
now = time.time()
|
||||
|
||||
if not success:
|
||||
self.attempts[identifier].append(now)
|
||||
# Clean old attempts
|
||||
self.attempts[identifier] = [
|
||||
t for t in self.attempts[identifier]
|
||||
if now - t < 3600 # 1 hour window
|
||||
]
|
||||
|
||||
# Lockout after 5 failures
|
||||
if len(self.attempts[identifier]) >= 5:
|
||||
self.lockouts[identifier] = now + 900 # 15 min lockout
|
||||
else:
|
||||
self.attempts[identifier] = []
|
||||
self.lockouts.pop(identifier, None)
|
||||
|
||||
def is_locked(self, identifier: str) -> bool:
|
||||
lockout_until = self.lockouts.get(identifier, 0)
|
||||
return time.time() < lockout_until
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Timing attacks on username enumeration
|
||||
- Account lockout as DOS vector
|
||||
- Session fixation attacks
|
||||
- Token leakage in logs/URLs
|
||||
- Password reset token reuse
|
||||
Reference in New Issue
Block a user