Common Vulnerability Patterns
Learn to identify common security vulnerability patterns during code review, organized by vulnerability category.
This section covers the most common vulnerability patterns you'll encounter during security code review, organized by category.
Injection Vulnerabilities
Injection occurs when untrusted data is sent to an interpreter as part of a command or query.
SQL Injection
Vulnerable pattern:
# String concatenation - VULNERABLE
query = "SELECT * FROM users WHERE id = " + user_id
cursor.execute(query)
# f-string interpolation - VULNERABLE
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
Secure pattern:
# Parameterized query - SECURE
query = "SELECT * FROM users WHERE id = %s"
cursor.execute(query, (user_id,))
# ORM with parameters - SECURE
User.query.filter_by(username=username).first()
Review checklist:
- Search for string concatenation near database calls
- Check for raw SQL queries with user input
- Verify ORM usage doesn't bypass parameterization
- Look for dynamic table/column names (harder to parameterize)
Command Injection
Vulnerable pattern:
# shell=True with user input - VULNERABLE
import subprocess
subprocess.run(f"ping {hostname}", shell=True)
# os.system - VULNERABLE
import os
os.system(f"convert {input_file} {output_file}")
Secure pattern:
# List arguments without shell - SECURE
import subprocess
subprocess.run(["ping", "-c", "4", hostname], shell=False)
# shlex.quote for unavoidable shell usage
import shlex
safe_hostname = shlex.quote(hostname)
Review checklist:
- Search for
shell=True,os.system,os.popen - Check subprocess calls for user-controlled arguments
- Look for eval(), exec() with external data
XSS (Cross-Site Scripting)
Vulnerable pattern:
// Direct DOM manipulation - VULNERABLE
document.getElementById('output').innerHTML = userInput;
// Template without escaping - VULNERABLE
const html = `<div>${userComment}</div>`;
# Jinja2 with safe filter on user input - VULNERABLE
{{ user_bio | safe }}
# Marking user input as safe - VULNERABLE
from markupsafe import Markup
return Markup(user_input)
Secure pattern:
// textContent instead of innerHTML - SECURE
document.getElementById('output').textContent = userInput;
// DOM APIs for element creation - SECURE
const div = document.createElement('div');
div.textContent = userComment;
# Default Jinja2 escaping - SECURE
{{ user_bio }}
# Explicit escaping
from markupsafe import escape
return escape(user_input)
Review checklist:
- Search for
innerHTML,outerHTML,document.write - Check for
| safefilter in templates - Look for
dangerouslySetInnerHTMLin React - Verify Content-Security-Policy headers exist
Authentication Issues
Weak Password Storage
Vulnerable pattern:
# Plain text - VULNERABLE
user.password = request.form['password']
# MD5/SHA1 - VULNERABLE (too fast, no salt)
import hashlib
user.password = hashlib.md5(password.encode()).hexdigest()
# SHA256 without salt - VULNERABLE
user.password = hashlib.sha256(password.encode()).hexdigest()
Secure pattern:
# bcrypt - SECURE (slow, salted)
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# Argon2 - SECURE (memory-hard, recommended)
from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash(password)
Review checklist:
- Search for hashlib with passwords
- Verify bcrypt, argon2, or scrypt is used
- Check that work factors are appropriate
- Ensure passwords are never logged
Session Management
Vulnerable pattern:
# Predictable session ID - VULNERABLE
session_id = str(user.id) + str(int(time.time()))
# Session fixation - VULNERABLE
# Not regenerating session after login
def login(user):
session['user_id'] = user.id
# Session ID stays the same!
Secure pattern:
# Cryptographically random session ID - SECURE
import secrets
session_id = secrets.token_urlsafe(32)
# Regenerate session after login - SECURE
def login(user):
session.regenerate() # New session ID
session['user_id'] = user.id
Review checklist:
- Verify session IDs are cryptographically random
- Check for session regeneration after authentication
- Verify session timeout is implemented
- Check for secure cookie flags (HttpOnly, Secure, SameSite)
Authorization Flaws
Missing Authorization Checks
Vulnerable pattern:
# No authorization check - VULNERABLE
@app.route('/api/users/<user_id>/profile')
def get_profile(user_id):
return User.query.get(user_id).to_dict()
# Check authentication but not authorization - VULNERABLE
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
# Any logged-in user can access any document!
return Document.query.get(doc_id).to_dict()
Secure pattern:
# Proper authorization check - SECURE
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
doc = Document.query.get(doc_id)
if doc.owner_id != current_user.id:
abort(403)
return doc.to_dict()
# Using scoped queries - SECURE
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
# Query automatically scoped to current user
doc = current_user.documents.filter_by(id=doc_id).first_or_404()
return doc.to_dict()
Review checklist:
- Verify every endpoint checks authorization
- Look for direct object references without ownership checks
- Check that authorization is server-side, not just client-side
- Verify admin functions check admin role
IDOR (Insecure Direct Object Reference)
Vulnerable pattern:
# Sequential IDs without authorization - VULNERABLE
@app.route('/invoice/<int:invoice_id>')
def get_invoice(invoice_id):
# Attacker can iterate through all invoice IDs
return Invoice.query.get(invoice_id)
Secure pattern:
# UUIDs + authorization check - SECURE
@app.route('/invoice/<uuid:invoice_id>')
@login_required
def get_invoice(invoice_id):
invoice = Invoice.query.filter_by(
id=invoice_id,
user_id=current_user.id
).first_or_404()
return invoice
Cryptographic Issues
Weak Algorithms
Vulnerable pattern:
# MD5 for integrity - VULNERABLE
import hashlib
checksum = hashlib.md5(data).hexdigest()
# DES encryption - VULNERABLE
from Crypto.Cipher import DES
cipher = DES.new(key, DES.MODE_ECB)
# ECB mode - VULNERABLE (patterns visible)
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
Secure pattern:
# SHA-256 for integrity - SECURE
import hashlib
checksum = hashlib.sha256(data).hexdigest()
# AES-GCM for encryption - SECURE (authenticated)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
Review checklist:
- Search for MD5, SHA1, DES, 3DES, RC4
- Check for ECB mode usage
- Verify IVs/nonces are random and not reused
- Check key sizes (AES-128 minimum, AES-256 preferred)
Hardcoded Secrets
Vulnerable pattern:
# Hardcoded in source - VULNERABLE
API_KEY = "sk_live_abc123xyz"
DB_PASSWORD = "supersecret123"
# Hardcoded in config file committed to git - VULNERABLE
# config.py
SECRET_KEY = "my-secret-key-12345"
Secure pattern:
# Environment variables - SECURE
import os
API_KEY = os.environ['API_KEY']
# Secrets manager - SECURE
from aws_secretsmanager import get_secret
DB_PASSWORD = get_secret('prod/db/password')
Review checklist:
- Search for common secret patterns (key, password, token, secret)
- Check config files for hardcoded values
- Verify secrets aren't logged or included in error messages
- Check git history for previously committed secrets
File Handling Issues
Path Traversal
Vulnerable pattern:
# Direct path concatenation - VULNERABLE
@app.route('/files/<filename>')
def get_file(filename):
return send_file(f'/uploads/{filename}')
# Attacker: /files/../../../etc/passwd
Secure pattern:
# Validate and sanitize path - SECURE
import os
from werkzeug.utils import secure_filename
@app.route('/files/<filename>')
def get_file(filename):
safe_name = secure_filename(filename)
file_path = os.path.join('/uploads', safe_name)
# Verify path is still within uploads directory
if not file_path.startswith('/uploads/'):
abort(400)
return send_file(file_path)
Review checklist:
- Search for file operations with user input
- Check for
..handling in paths - Verify paths are canonicalized before comparison
- Look for symlink following issues
Unrestricted File Upload
Vulnerable pattern:
# No validation - VULNERABLE
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
file.save(f'/uploads/{file.filename}')
Secure pattern:
# Validate type, size, and sanitize name - SECURE
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_SIZE = 5 * 1024 * 1024 # 5MB
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['file']
# Check extension
ext = file.filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
abort(400, 'Invalid file type')
# Check content type (not just extension)
if not file.content_type.startswith('image/'):
abort(400, 'Invalid content type')
# Generate safe filename
safe_name = f"{uuid.uuid4()}.{ext}"
# Save outside web root
file.save(f'/data/uploads/{safe_name}')
Review checklist:
- Verify file type validation (extension AND content)
- Check for size limits
- Ensure files are saved outside web root
- Look for filename sanitization
Found an issue?