Common Security Anti-Patterns
When to Use
Learn from others' mistakes. These anti-patterns represent the most common security failures that lead to breaches.
Security Through Obscurity
What it is: Relying on secrecy of implementation details instead of strong security controls.
# Bad: Hidden admin endpoint
@app.route('/super_secret_admin_panel_xyz123')
def admin():
return render_template('admin.html') # No authentication
# Good: Obscurity + real security
@app.route('/admin')
@require_authentication
@require_role('admin')
def admin():
return render_template('admin.html')
Real-world impact: 2017 Equifax breach — attackers found admin portal via directory traversal, no auth required.
Blacklist Input Validation
What it is: Trying to block known-bad inputs instead of allowing known-good inputs.
# Bad: Blacklist dangerous characters
def sanitize_bad(user_input):
dangerous = ['<script>', 'javascript:', 'onerror=']
for pattern in dangerous:
user_input = user_input.replace(pattern, '')
return user_input
# Bypasses: <scr<script>ipt>, <ScRiPt>, %3Cscript%3E, <img src=x onerror=alert(1)>
# Good: Allowlist validation
def sanitize_good(user_input):
if re.match(r'^[a-zA-Z0-9_-]{3,20}$', user_input):
return user_input
raise ValueError("Invalid input")
Trusting Client-Side Validation
What it is: Relying on JavaScript validation as a security control.
<!-- Bad: Client-side only -->
<script>
function validateForm() {
const price = document.getElementById('price').value;
if (price < 0 || price > 1000) { return false; }
return true;
}
</script>
<!-- Attacker: curl -X POST -d "price=9999999" https://example.com/checkout -->
# Good: Server-side validation (REQUIRED)
@app.route('/checkout', methods=['POST'])
def checkout():
price = request.form.get('price', type=float)
if price is None or price < 0 or price > 1000:
return {"error": "Invalid price"}, 400
Real-world impact: 2019 British Airways breach — client-side payment validation bypassed, $230M GDPR fine.
Insufficient Password Complexity
# Bad: Weak requirements
def validate_password_bad(password):
if len(password) >= 8:
return True # Allows: "password", "12345678", "qwerty123"
# Good: Strong requirements + compromised password check
def validate_password_good(password):
if len(password) < 12:
raise ValueError("Password must be at least 12 characters")
# Check against HaveIBeenPwned (k-anonymity API)
sha1_hash = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1_hash[:5], sha1_hash[5:]
response = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
for line in response.text.split('\n'):
if suffix in line:
raise ValueError("Password compromised in a data breach")
return True
Real-world impact: 81% of data breaches involve weak/stolen passwords (Verizon DBIR 2025).
Not Encrypting Sensitive Data
-- Bad: Plaintext sensitive data
CREATE TABLE users (
ssn VARCHAR(11), -- Plaintext SSN
credit_card VARCHAR(19) -- Plaintext credit card
);
-- Good: Encrypted sensitive fields
CREATE TABLE users (
ssn_encrypted TEXT, -- AES-256-GCM encrypted
-- Better: Don't store credit cards, use tokenization
);
Real-world impact: 2013 Target breach — 40M credit cards stolen, $18.5M settlement.
Using Deprecated Crypto
# Bad: MD5 for passwords (fast, no salt, rainbow tables exist)
password_hash = hashlib.md5(password.encode()).hexdigest()
# Bad: Weak key lengths
rsa_key = rsa.generate_private_key(key_size=1024) # Broken
# Good: Modern algorithms
from argon2 import PasswordHasher
ph = PasswordHasher()
password_hash = ph.hash(password)
rsa_key = rsa.generate_private_key(key_size=4096)
Real-world impact: 2012 LinkedIn breach — 6.5M SHA-1 hashed passwords cracked within days (no salt).
Inadequate Session Management
# Bad: Predictable session ID
session_id = str(user.id) + "_" + datetime.now().strftime("%Y%m%d")
# Good: Secure session management
session_id = secrets.token_hex(32) # Cryptographically random
# Regenerate after login, expire sessions, invalidate on logout
Verbose Error Messages
# Bad: Detailed error in production
except Exception as e:
return f"Database error: {str(e)}", 500
# Reveals: MySQL database, vulnerable to SQL injection
# Good: Generic error for users, detailed logs server-side
except Exception as e:
logger.error(f"Database query failed: {e}")
return {"error": "An error occurred. Please try again later."}, 500
No Rate Limiting
# Bad: No rate limiting on login
@app.route('/login', methods=['POST'])
def login():
if authenticate(username, password):
return redirect('/dashboard')
return 'Invalid credentials', 401
# Attacker tries 10,000 passwords/second
# Good: Rate limiting
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# ... same logic ...
Common Mistakes
- Assuming users are non-technical — Attackers are sophisticated
- Copy-pasting code without understanding — Stack Overflow answers may have security flaws
- Not learning from breaches — Every major breach has post-mortem analysis. Read them
- Thinking "we're too small to be targeted" — Automated scanners target EVERYONE
- Security by compliance checkbox — PCI DSS compliance doesn't prevent breaches
See Also
- Previous: Secure Development Lifecycle | Next: Security Checklist
- Reference: OWASP Top 10 Proactive Controls