Security Audit Report

Vulnerable Flask Application

Language: Python | Framework: Flask

Tools: Bandit, Manual Inspection

Standard: OWASP Top 10 (2021)

Section 1 — Vulnerable Flask App (Code Review)

The following Python Flask application was selected as the audit target. It intentionally contains nine security vulnerabilities across six OWASP Top 10 categories. Each vulnerability is annotated in the source code with a warning comment.

1.1 Application Overview

Framework: Python / Flask | Database: SQLite (in-memory) | Auth: Session-based

The app exposes the following routes:

  • /login — Authentication with SQL Injection vulnerability
  • /greet — Greeting page with XSS vulnerability
  • /search — User search with SQL Injection vulnerability
  • /load_profile — Profile loader with Insecure Deserialization
  • /read_file — File reader with Path Traversal vulnerability
  • /admin — Admin panel with Broken Access Control
  • /dashboard — User dashboard

1.2 Vulnerability 1 — SQL Injection (Login)

Severity: HIGH

Location: /login route, line ~52

OWASP: A03:2021 — Injection

Description: User-supplied username and password are directly interpolated into an SQL query string using an f-string. An attacker can manipulate the query logic to bypass authentication entirely.

Vulnerable Code

query = f"SELECT * FROM users WHERE username = '{username}'
AND password = '{password}'"
c.execute(query)

Proof of Concept

Enter the following in the username field, with any password:

' OR '1'='1' -

1.3 Vulnerability 2 — SQL Injection (Search)

Severity: HIGH

Location: /search route, line ~85

OWASP: A03:2021 — Injection

Description: The search query parameter is interpolated directly into a LIKE clause, allowing an attacker to dump all records or manipulate query logic.

c.execute(f"SELECT username, role FROM users
WHERE username LIKE '%{query_param}%'")

1.4 Vulnerability 3 — Hardcoded Secret Key

Severity: HIGH

Location: Line ~30 (app config)

OWASP: A02:2021 — Cryptographic Failures

Description: The Flask secret key is hardcoded as a string literal in source code. Anyone with access to the code can forge session cookies and impersonate any user.

app.secret_key = "supersecretkey123"

1.5 Vulnerability 4 — Hardcoded Credentials

Severity: HIGH

Location: init_db() function, line ~38

OWASP: A07:2021 — Identification and Authentication Failures

Description: The admin password is hardcoded in plaintext inside the source code and stored without any hashing. This exposes credentials to anyone who can read the source or database.

c.execute("INSERT INTO users VALUES
(1, 'admin', 'admin123', 'admin')")

1.6 Vulnerability 5 — Cross-Site Scripting (XSS)

Severity: MEDIUM

Location: /greet route

OWASP: A03:2021 — Injection

Description: The Jinja2 |safe filter is applied to a user-supplied URL parameter, disabling auto-escaping. An attacker can inject a <script> tag that executes in the victim's browser.

return render_template_string(
"<h2>Hello, {{ name|safe }}!</h2>", name=name
)

Proof of concept URL:

/greet?name=<script>alert('XSS')</script>

1.7 Vulnerability 6 — Insecure Deserialization

Severity: HIGH

Location: /load_profile route

OWASP: A08:2021 — Software and Data Integrity Failures

Description: pickle.loads() is called on raw user-supplied request body bytes. A malicious pickle payload can execute arbitrary operating system commands on the server.

data = request.data # raw bytes from user
profile = pickle.loads(data) # DANGEROUS

1.8 Vulnerability 7 — Path Traversal

Severity: HIGH

Location: /read_file route

OWASP: A01:2021 — Broken Access Control

Description: The filename parameter is passed directly to os.path.join without validation. An attacker can use ../ sequences to traverse outside the intended directory and read arbitrary files.

filename = request.args.get('filename', '')
filepath = os.path.join(base_dir, filename) # no sanitization
# Attack: /read_file?filename=../../etc/passwd

1.9 Vulnerability 8 — Broken Access Control (Admin)

Severity: MEDIUM

Location: /admin route

OWASP: A01:2021 — Broken Access Control

Description: The admin route only checks whether a session key exists, not whether the user's role is 'admin'. Any authenticated user can access the admin panel.

if "user" in session: # only checks login, not role
return admin_panel()

1.10 Vulnerability 9 — Debug Mode Enabled

Severity: MEDIUM

Location: app.run(debug=True)

OWASP: A05:2021 — Security Misconfiguration

Description: Running Flask with debug=True exposes the Werkzeug interactive debugger in the browser. If triggered in production, it allows an attacker to execute arbitrary Python code remotely.

app.run(debug=True) # NEVER use in production

Section 2 — Recommendations & Best Practices

The following table maps each vulnerability to its recommended fix and the relevant OWASP reference.

None

2.1 General Secure Coding Principles

  • Never trust user input — validate and sanitize everything from the browser or API
  • Apply the principle of least privilege — users should only access what they need
  • Keep dependencies up to date — run 'pip install safety && safety check' regularly
  • Store secrets in environment variables, never in source code
  • Use HTTPS in production to protect data in transit
  • Use a Web Application Firewall (WAF) as a secondary defence layer
  • Follow the OWASP Top 10 as your baseline security checklist

Section 3 — Findings & Remediation Steps

3.1 Summary of Findings

None

3.2 Fix 1 — SQL Injection

Before (Vulnerable)

query = f"SELECT * FROM users WHERE username = '{username}'
AND password = '{password}'"
c.execute(query)

After (Fixed)

query = "SELECT * FROM users WHERE username = ? AND password = ?"
c.execute(query, (username, password)) # parameterized query

3.3 Fix 2 — Hardcoded Secret Key

Before (Vulnerable)

app.secret_key = "supersecretkey123"

After (Fixed)

import os
from dotenv import load_dotenv
load_dotenv()
app.secret_key = os.environ.get("SECRET_KEY")

3.4 Fix 3 — Hardcoded Credentials

Before (Vulnerable)

c.execute("INSERT INTO users VALUES (1, 'admin', 'admin123', 'admin')")

After (Fixed)

from werkzeug.security import generate_password_hash
hashed = generate_password_hash(os.environ.get("ADMIN_PASSWORD"))
c.execute("INSERT INTO users VALUES (1, 'admin', ?, 'admin')", (hashed,))

3.5 Fix 4 — Cross-Site Scripting (XSS)

Before (Vulnerable)

return render_template_string("<h2>Hello, {{ name|safe }}!</h2>", name=name)

After (Fixed)

#Remove |safe - Jinja2 auto-escapes by default
return render_template_string("<h2>Hello, {{ name }}!</h2>", name=name)

3.6 Fix 5 — Insecure Deserialization

Before (Vulnerable)

profile = pickle.loads(request.data)

After (Fixed)

import json
profile = json.loads(request.data) # safe alternative to pickle

3.7 Fix 6 — Path Traversal

Before (Vulnerable)

filepath = os.path.join(base_dir, filename) # no validation

After (Fixed)

safe_path = os.path.realpath(os.path.join(base_dir, filename))
if not safe_path.startswith(os.path.realpath(base_dir)):
return "Access denied", 403
filepath = safe_path

3.8 Fix 7 — Broken Access Control

Before (Vulnerable)

if "user" in session:

After (Fixed)

if session.get("role") != "admin":
return "Forbidden", 403

3.9 Fix 8 — Debug Mode

Before (Vulnerable)

app.run(debug=True)

After (Fixed)

app.run(debug=os.environ.get("FLASK_DEBUG", "false") == "true")

Conclusion

The reviewed Flask application contained nine security vulnerabilities across six OWASP Top 10 categories. Five findings were rated High severity, including SQL Injection, Insecure Deserialization, and Path Traversal — all of which could lead to full system compromise if exploited in a production environment.

Remediation steps have been provided for every finding. Priority should be given to the SQL Injection, Insecure Deserialization, and Hardcoded Secrets vulnerabilities due to their critical potential impact.

Running Bandit static analysis is recommended as an ongoing practice:

pip install bandit
bandit -r vulnerable_app.py -f txt -o bandit_report.txt