Skip to main content
This page provides detailed side-by-side comparisons of vulnerable and secure code implementations, highlighting the specific changes needed to fix each vulnerability.

SQL Injection

Login Authentication

# VULNERABLE: SQL Injection
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
print(f"Query ejecutada: {query}")

try:
    cursor.execute(query)
    user = cursor.fetchone()
Vulnerable Code Issues:
  1. String Interpolation: Uses f-string to build query
    f"SELECT * FROM users WHERE username = '{username}'"
    
  2. Direct Password Comparison: Compares plaintext passwords in SQL
    AND password = '{password}'
    
  3. Debug Output: Prints queries to console
    print(f"Query ejecutada: {query}")
    
Attack Vector:
Username: admin' OR '1'='1' --
Password: anything

# Results in:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = 'anything'
# The -- comments out the rest, OR '1'='1' is always true

Profile Lookup

user_id = request.args.get('id', session['user_id'])

connection = create_connection()
cursor = connection.cursor() 

try:
    cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
    user = cursor.fetchone()
Two Vulnerabilities Fixed:
  1. SQL Injection - Parameterized query
  2. IDOR - Authorization check before database query

Cross-Site Scripting (XSS)

Dashboard Message Display

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        flash('Debes iniciar sesión', 'warning')
        return redirect('/login')
    
    # VULNERABLE: XSS
    message = request.args.get('message', '')
    
    return render_template('dashboard.html', 
                         username=session.get('username'),
                         message=message,
                         role=session.get('role'))

How escape() Works

The markupsafe.escape() function converts HTML special characters:
InputOutputWhy
<&lt;Start of HTML tag
>&gt;End of HTML tag
"&quot;Attribute delimiter
'&#x27;Attribute delimiter
&&amp;Entity start
Example:
input: "<script>alert('XSS')</script>"
output: "&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;"
Rendered as harmless text: <script>alert('XSS')</script>

Password Storage

User Registration

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']  # Plaintext
        email = request.form['email']
        
        connection = create_connection()
        cursor = connection.cursor()
        
        try:
            query = "INSERT INTO users (username, password, email) VALUES (%s, %s, %s)"
            cursor.execute(query, (username, password, email))  # Stores plaintext
            connection.commit()

Database Storage Comparison

Plaintext Password Storage:
INSERT INTO users (username, password, email) 
VALUES ('admin', 'admin123', '[email protected]')
Database Contents:
id | username | password   | email
---+----------+------------+------------------
1  | admin    | admin123   | [email protected]
2  | usuario  | password123| [email protected]
Risks:
  • Database breach exposes all passwords immediately
  • Admins can see user passwords
  • No protection against rainbow tables
  • Violates GDPR and most compliance standards
  • Users reusing passwords across sites are compromised

Password Verification

# Password checked in SQL query
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
user = cursor.fetchone()

if user:
    # Login successful
How check_password_hash() Works:
stored_hash = "scrypt:32768:8:1$h3k2j1$f8e2a9..."
user_input = "admin123"

# Extract algorithm and salt from stored hash
algorithm, params, salt, original_hash = parse_hash(stored_hash)

# Re-hash the input with same salt and params
new_hash = scrypt(user_input, salt, params)

# Constant-time comparison (prevents timing attacks)
return secure_compare(new_hash, original_hash)

Insecure Direct Object Reference (IDOR)

Authorization Implementation

@app.route('/profile')
def profile():
    if 'user_id' not in session:
        flash('Debes iniciar sesión', 'warning')
        return redirect('/login')
    
    # No authorization check!
    user_id = request.args.get('id', session['user_id'])
    
    connection = create_connection()
    cursor = connection.cursor() 
    
    try:
        # SQL injection + IDOR
        cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
        user = cursor.fetchone()
    except sqlite3.Error as e:
        flash(f'Error: {str(e)}', 'danger')
        user = None
    finally:
        cursor.close()
        connection.close()
    
    return render_template('profile.html', user=user)

Authorization Logic Breakdown

Problems:
  • ❌ No permission check
  • ❌ Any authenticated user can view any profile
  • ❌ Returns password field
  • ❌ SQL injection in ID parameter

Data Minimization

SELECT * FROM users WHERE id = 1
-- Returns: id, username, password, email, role, created_at
-- ❌ Password field exposed!
Principle of Least Privilege: Only query and return the data actually needed. Even with hashed passwords, there’s no reason to send them to the frontend.

Security Headers

No Headers vs Complete Headers

from flask import Flask

app = Flask(__name__)
app.secret_key = 'clave_super_secreta_123'

# No security headers configured ❌

@app.route('/')
def index():
    return render_template('index.html')

Header-by-Header Comparison

Purpose: Prevent MIME type sniffingWithout Header:
HTTP/1.1 200 OK
Content-Type: text/html
Browser might interpret text/html file as text/javascript if it contains code.With Header:
HTTP/1.1 200 OK
Content-Type: text/html
X-Content-Type-Options: nosniff
Browser strictly follows declared Content-Type.
Attack Prevented: MIME confusion attacks where attackers upload malicious files disguised as safe types

Configuration & Secrets Management

Secret Key Comparison

app.secret_key = 'clave_super_secreta_123'

Environment Variable Setup

Hardcoded in Source:
# app.py (committed to git)
app.secret_key = 'clave_super_secreta_123'
DATABASE_URL = 'postgresql://admin:password@localhost/db'
API_KEY = 'sk_live_abc123xyz789'
Problems:
  • ❌ Visible in version control history forever
  • ❌ Same secrets in dev/staging/production
  • ❌ Developers can see production secrets
  • ❌ Leaked if repository becomes public
  • ❌ No rotation without code changes

Input Validation

Registration Form Validation

username = request.form['username']  # No validation
password = request.form['password']  # No validation
email = request.form['email']       # No validation

# Directly inserted into database
cursor.execute(query, (username, password, email))

Validation Checklist

Check if fields exist and are not empty:
# Bad - KeyError if field missing
username = request.form['username']

# Good - Returns empty string if missing
username = request.form.get('username', '')

# Best - Also checks for empty
username = request.form.get('username', '').strip()
if not username:
    return error('Username is required')
Enforce minimum and maximum lengths:
if len(username) < 3:
    return error('Username too short')

if len(username) > 50:
    return error('Username too long')

# Or combined:
if not (3 <= len(username) <= 50):
    return error('Username must be 3-50 characters')
Why length limits matter:
  • Prevent database overflow
  • Limit resource consumption
  • Block certain attack patterns
Ensure correct data types:
# Validate integer
user_id = request.args.get('id')
try:
    user_id = int(user_id)
except (ValueError, TypeError):
    return error('Invalid ID format')

# Validate email format
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
    return error('Invalid email format')
Trim whitespace to prevent issues:
# Without strip()
username = '  admin  '
# Stored as '  admin  ' (different from 'admin')
# Can bypass duplicate checks

# With strip()
username = request.form.get('username', '').strip()
# Stored as 'admin' (normalized)

Summary Table: All Vulnerabilities

VulnerabilityVulnerable CodeSecure CodeImpact
SQL Injectionf"SELECT * FROM users WHERE id = {id}"cursor.execute("SELECT * FROM users WHERE id = %s", (id,))Critical - Full database compromise
XSS{{ message|safe }}{{ escape(message) }}High - Account takeover, data theft
IDORNo authorization checkif id != session['user_id'] and role != 'admin': abort(403)High - Unauthorized data access
Plaintext Passwordspassword = 'admin123'password = generate_password_hash('admin123')Critical - Mass password breach
Hardcoded Secretsapp.secret_key = 'secret'app.secret_key = os.getenv('SECRET_KEY')High - Session compromise
Missing CSRFNo protectioncsrf = CSRFProtect(app)Medium - Forced actions
No Security HeadersNo headers@app.after_request middlewareMedium - Multiple attack vectors
Poor Input Validationrequest.form['username']request.form.get('username', '').strip() + validationMedium - Data integrity, DoS

Testing Guide

1

Deploy Both Applications

# Terminal 1: Vulnerable app
cd vulnerable
python app.py  # Runs on port 5000

# Terminal 2: Secure app
cd secure
python app.py  # Runs on port 5001
2

Test SQL Injection

Vulnerable (port 5000):
  • Username: admin' OR '1'='1' --
  • Password: anything
  • Result: ✅ Logged in (bypassed authentication)
Secure (port 5001):
  • Username: admin' OR '1'='1' --
  • Password: anything
  • Result: ❌ “Credenciales incorrectas”
3

Test XSS

Vulnerable (port 5000):
http://localhost:5000/dashboard?message=<script>alert('XSS')</script>
  • Result: ⚠️ Alert popup (JavaScript executed)
Secure (port 5001):
http://localhost:5001/dashboard?message=<script>alert('XSS')</script>
  • Result: ✅ Text displayed: <script>alert('XSS')</script>
4

Test IDOR

Log in as regular user, then:Vulnerable (port 5000):
http://localhost:5000/profile?id=1
  • Result: ✅ Shows admin profile with password
Secure (port 5001):
http://localhost:5001/profile?id=1
  • Result: ❌ “No tienes permiso para ver este perfil”
5

Inspect Database

# Vulnerable database
sqlite3 vulnerable/users.db "SELECT * FROM users"
# Shows plaintext passwords

# Secure database
sqlite3 secure/users.db "SELECT * FROM users"
# Shows hashed passwords (scrypt:...)
All security improvements are demonstrated through working code. Run both applications side-by-side to see the difference in behavior.

Build docs developers (and LLMs) love