OWASP Top Concerns for Python Web Apps
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.'
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_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)
The Output
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
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.