← Python Code Performance & Security
Browse Python Concepts

OWASP Top Concerns for Python Web Apps

Mental Model

Think of user input as an unverified visitor trying to access restricted areas of your application. Without proper checks and escorts (validation and sanitization), this visitor can bypass security, access sensitive data, or even control your application by exploiting its trust in their 'identity.'

Rule: Always validate and sanitize all user input, especially for database queries (use parameterized queries) and external resource requests (whitelist URLs and schemes), to prevent OWASP Top 10 vulnerabilities.

The Setup

A Python-based API service handles user authentication and data retrieval. The team is under pressure to deliver features quickly, leading to shortcuts in data handling and validation, especially for direct database interactions or external API calls.

What Does This Print?

Broken code
Python
# broken_api.py (simplified Flask/FastAPI style for illustration)
from flask import Flask, request, jsonify # Using Flask for example syntax
import sqlite3
import requests # For SSRF example

app = Flask(__name__)
DATABASE = 'users.db'

def get_db_connection():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    conn = get_db_connection()
    # BROKEN: SQL Injection vulnerability - directly embedding user input
    cursor = conn.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'")
    user = cursor.fetchone()
    conn.close()

    if user:
        return jsonify({"message": "Login successful", "user": dict(user)}), 200
    return jsonify({"message": "Invalid credentials"}), 401

@app.route('/fetch_url', methods=['GET'])
def fetch_url():
    url = request.args.get('url')
    if not url:
        return jsonify({"error": "URL parameter missing"}), 400
    
    # BROKEN: Server-Side Request Forgery (SSRF) vulnerability - fetching arbitrary URLs
    try:
        response = requests.get(url, timeout=5)
        return jsonify({"content": response.text}), 200
    except requests.exceptions.RequestException as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    # Initialize DB for demo
    conn = sqlite3.connect(DATABASE)
    conn.execute("DROP TABLE IF EXISTS users")
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)")
    conn.execute("INSERT INTO users (username, password) VALUES ('admin', 'password123')")
    conn.commit()
    conn.close()
    print("Database initialized with user 'admin', password 'password123'")
    app.run(debug=True, port=5000)
Identify the specific OWASP Top 10 vulnerabilities present in the Flask application above. Describe how an attacker could exploit each to gain unauthorized access or information.

The Output

What actually happens
Exploiting SQL Injection: POST to /login with {"username": "' OR 1=1 --", "password": "any"} Exploiting SSRF: GET to /fetch_url?url=http://localhost:5000/login

The application exhibits two critical OWASP Top 10 vulnerabilities: 1. A03:2021-Injection (SQL Injection): The /login endpoint constructs a SQL query string directly using f-strings and user-supplied input for username and password. An attacker could inject malicious SQL, e.g., username='admin'-- and any password, or username='admin' OR 1=1-- to bypass authentication. 2. A10:2021-Server-Side Request Forgery (SSRF): The /fetch_url endpoint fetches an arbitrary URL provided by the user. An attacker could supply internal network URLs (e.g., http://localhost/admin or http://169.254.169.254/latest/meta-data/) to access sensitive internal resources or cloud metadata services.

Why Python Does This

Python itself is not inherently insecure; these vulnerabilities arise from insecure coding practices, not language flaws. SQL injection occurs because the database driver interprets user input as part of the SQL command rather than as a literal string value. This happens when query strings are built via string concatenation or f-strings instead of parameterized queries, which explicitly separate code from data. SSRF arises because the application's network requests library (requests in this case) is given an arbitrary, unvalidated URL. The library faithfully executes the request from the server's perspective, allowing access to resources typically unreachable by the client. Python's flexibility and powerful string manipulation (f-strings) and network libraries (requests) are powerful tools that, when misused, enable these security flaws.

The Fix

Corrected pattern
Python
from flask import Flask, request, jsonify
import sqlite3
import requests
from urllib.parse import urlparse # FIX: For SSRF URL validation

app = Flask(__name__)
DATABASE = 'users.db'

def get_db_connection():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    return conn

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    conn = get_db_connection()
    # FIX: Use parameterized queries to prevent SQL Injection
    cursor = conn.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
    user = cursor.fetchone()
    conn.close()

    if user:
        return jsonify({"message": "Login successful", "user": dict(user)}), 200
    return jsonify({"message": "Invalid credentials"}), 401

# FIX: Whitelist of allowed domains for SSRF mitigation
ALLOWED_DOMAINS = ['example.com', 'api.external.com'] 

def is_safe_url(url):
    try:
        parsed_url = urlparse(url)
        # Check scheme, network location, and prevent file/internal schemes
        if parsed_url.scheme not in ['http', 'https']:
            return False
        # Check if the domain is in our allowed list
        if parsed_url.netloc not in ALLOWED_DOMAINS:
            return False
        return True
    except ValueError:
        return False

@app.route('/fetch_url', methods=['GET'])
def fetch_url():
    url = request.args.get('url')
    if not url:
        return jsonify({"error": "URL parameter missing"}), 400
    
    # FIX: Validate URL to prevent SSRF
    if not is_safe_url(url):
        return jsonify({"error": "Unsafe URL provided"}), 403 # Forbidden
    
    try:
        response = requests.get(url, timeout=5)
        return jsonify({"content": response.text}), 200
    except requests.exceptions.RequestException as e:
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    conn = sqlite3.connect(DATABASE)
    conn.execute("DROP TABLE IF EXISTS users")
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)")
    conn.execute("INSERT INTO users (username, password) VALUES ('admin', 'password123')")
    conn.commit()
    conn.close()
    print("Database initialized with user 'admin', password '123'")
    app.run(debug=True, port=5000)

Using parameterized queries (e.g., cursor.execute("SELECT ... WHERE username = ? AND password = ?", (username, password))) separates the SQL logic from the data, preventing malicious input from being interpreted as code. For SSRF, whitelisting allowed URLs or domains and strictly parsing input ensures that your application only fetches from approved, safe locations, blocking attempts to access internal networks or sensitive endpoints.

How This Fails in Real Systems

A legacy Python 2 web application, running on an internal corporate network, exposed an endpoint that allowed users to fetch "reports" by URL. This endpoint, intended for whitelisted internal services, eventually had its validation relaxed. An attacker discovered this, used the SSRF vulnerability to scan the internal network, and eventually accessed unauthenticated metadata services on cloud instances, extracting AWS IAM role credentials. This led to a full compromise of several production databases before the access patterns were flagged by anomaly detection, taking over a month to fully remediate.

Key Takeaway

Always validate and sanitize all user input, especially for database queries (use parameterized queries) and external resource requests (whitelist URLs and schemes), to prevent OWASP Top 10 vulnerabilities.
Common mistake: Developers trust or directly embed user-supplied input into database queries, file paths, or external resource requests, opening severe security holes like SQL Injection or Server-Side Request Forgery.