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 | safe filter in templates
  • Look for dangerouslySetInnerHTML in 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?