Python Security Mistakes Developers Make
Python's simplicity hides serious security traps. These 5 mistakes appear in production Python code every day — from Django apps to data pipelines — and each one can lead to data breaches, server compromise, or unauthorized access.
Affects: Django · Flask · FastAPI · Scripts · Data pipelines
Hardcoded Secrets
Hardcoding API keys, database passwords, or JWT secrets directly in source code is one of the most common — and most costly — Python security mistakes. Once code is committed to git, the secret is permanently in history even if deleted later.
❌ Vulnerable
import openai openai.api_key = "sk-proj-abc123..." # ← Exposed in git history forever AWS_SECRET = "wJalrXUtnFEMI/K7MDENGbPxRfi..." DB_PASSWORD = "supersecret123"
✅ Secure Fix
import os
import openai
openai.api_key = os.environ["OPENAI_API_KEY"] # ← From environment
# Or use python-dotenv for local development:
from dotenv import load_dotenv
load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY")Pro tip: Add .env to .gitignore immediately. Use tools like Doppler, AWS Secrets Manager, or Vault in production. Run git secrets --scan before every commit.
Command Injection via subprocess
When Python code passes user input directly to a shell command, an attacker can inject arbitrary commands. This gives full access to the server — read files, exfiltrate data, install malware. It ranks #3 in OWASP Top 10 (Injection).
❌ Vulnerable
import subprocess
filename = request.args.get("file") # User input
# Attacker sends: file=report.txt; cat /etc/passwd
result = subprocess.run(f"cat {filename}", shell=True, capture_output=True)✅ Secure Fix
import subprocess
import os
filename = request.args.get("file")
# Pass as list — no shell interpretation
result = subprocess.run(["cat", filename], capture_output=True, text=True)
# Even better: validate the path first
safe_dir = "/var/app/reports"
safe_path = os.path.realpath(os.path.join(safe_dir, filename))
if not safe_path.startswith(safe_dir):
raise ValueError("Path traversal detected")
result = subprocess.run(["cat", safe_path], capture_output=True)Rule: Never use shell=True with any user-controlled input. Pass command arguments as a list, not a string. See the full guide: Python Command Injection →
SQL Injection via String Formatting
Embedding user input directly in SQL queries via f-strings or % formatting is the #1 cause of data breaches. Attackers can bypass login, dump entire databases, or delete all data.
❌ Vulnerable
# Attacker sends: username = ' OR '1'='1
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query) # ← Full database exposed✅ Secure Fix
# Parameterized query — always
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,) # ← Driver safely escapes all input
)
# SQLAlchemy ORM (preferred for complex apps):
user = db.session.query(User).filter_by(username=username).first()Golden rule: Never concatenate or format user input into SQL. Always use parameterized queries or an ORM. The %s placeholder works regardless of the value — even if it contains quotes or semicolons.
Path Traversal via User-Controlled File Paths
When a user can control a file path (download endpoint, file upload, log viewer), they can use ../ sequences to escape the intended directory and read arbitrary files like /etc/passwd or application secrets.
❌ Vulnerable
from flask import Flask, send_file, request
app = Flask(__name__)
@app.route('/download')
def download():
filename = request.args.get('file')
# Attacker sends: file=../../etc/passwd
return send_file(f"/var/app/uploads/{filename}")✅ Secure Fix
import os
from flask import abort
UPLOAD_DIR = "/var/app/uploads"
@app.route('/download')
def download():
filename = request.args.get('file')
# Resolve the real path and verify it's inside UPLOAD_DIR
safe_path = os.path.realpath(os.path.join(UPLOAD_DIR, filename))
if not safe_path.startswith(UPLOAD_DIR + os.sep):
abort(403) # Forbidden
return send_file(safe_path)Missing Input Validation on API Endpoints
Python APIs that accept JSON without validating structure, types, or bounds are vulnerable to unexpected data causing crashes, incorrect behavior, or business logic bypasses.
❌ Vulnerable
@app.route('/transfer', methods=['POST'])
def transfer():
data = request.json
amount = data['amount'] # Could be negative, string, or missing
to_user = data['to'] # Could be any value
# Attacker sends: {"amount": -9999, "to": "attacker"}✅ Secure Fix
from pydantic import BaseModel, Field, validator
class TransferRequest(BaseModel):
amount: float = Field(gt=0, le=10000) # Must be > 0 and ≤ 10000
to: str = Field(min_length=3, max_length=50)
@validator('to')
def to_must_be_valid_username(cls, v):
if not v.isalnum():
raise ValueError('Invalid username')
return v
@app.route('/transfer', methods=['POST'])
def transfer():
try:
req = TransferRequest(**request.json)
except ValidationError as e:
return {"error": str(e)}, 400
# Safe to use req.amount and req.toUse Pydantic for all API input validation — it handles type coercion, validation, and error messages automatically. FastAPI has it built in. For Django, use Django REST Framework serializers.
Python Security Review Checklist
Check your Python code for all of these issues
Paste any Python snippet — LearnCodeGuide detects security vulnerabilities, bugs, and quality issues automatically.
Detect Python Security Issues →Related Guides
Published by LearnCodeGuide Team · Last reviewed: October 2025