Python Security Best Practices
Security is not an afterthought — it is a design decision that affects every layer of your application. This guide covers practical security patterns for Python developers, from handling user input to managing secrets.
Validate All Input
Never trust user input. Every piece of data that enters your application from an external source is a potential attack vector.
Use a Validation Library
Pydantic is the standard for data validation in Python:
from pydantic import BaseModel, EmailStr, Field, validator
class UserRegistration(BaseModel):
username: str = Field(min_length=3, max_length=30)
email: EmailStr
password: str = Field(min_length=8)
@validator("username")
def username_alphanumeric(cls, v):
assert v.isalnum(), "must be alphanumeric"
return v.lower()
# Rejects invalid email, weak passwords, bad usernames
user = UserRegistration(
username="john123",
email="john@example.com",
password="secret123"
)
Sanitize SQL Queries
Never interpolate user input directly into SQL. Use parameterized queries:
# Bad — vulnerable to SQL injection
query = f"SELECT * FROM users WHERE username = + username + "
# Good — safe from injection
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
With SQLAlchemy ORM:
# Use the ORM which handles escaping automatically
user = session.query(User).filter(User.username == username).first()
Handle Secrets Properly
Never hardcode secrets in your source code. Use environment variables or a secrets manager.
Environment Variables
import os
# Access secrets from environment
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
API_KEY = os.environ.get("API_KEY")
if not API_KEY:
raise ValueError("API_KEY environment variable is required")
Python-Dotenv for Development
# .env file (add to .gitignore)
# DATABASE_PASSWORD=secret123
# API_KEY=xxx
from dotenv import load_dotenv
load_dotenv() # Loads .env file into environment
Avoid Accidental Secret Leaks
Be careful with logging and error messages:
# Bad — leaks credentials in logs
logger.info(f"Connecting with password: {password}")
# Good — no sensitive data in logs
logger.info("Connecting to database")
Password Handling
Never store passwords in plain text. Use strong hashing algorithms.
Using Passlib
from passlib.hash import bcrypt
# Hash a password for storage
hashed = bcrypt.hash("user_password")
# $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyY1KKE9YFdm
# Verify a password
bcrypt.verify("user_password", hashed) # True
bcrypt.verify("wrong_password", hashed) # False
With Bcrypt Module Directly
import bcrypt
# Hashing
password = b"secret_password"
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
# Verifying
bcrypt.checkpw(password, hashed) # True
Secure File Handling
File operations can expose your system to path traversal attacks.
Prevent Path Traversal
import os
from pathlib import Path
def safe_read_file(filename: str, base_dir: Path):
# Resolve the full path
full_path = (base_dir / filename).resolve()
# Ensure it stays within the base directory
if not str(full_path).startswith(str(base_dir)):
raise ValueError("Invalid file path")
return full_path.read_text()
# Usage
BASE_DIR = Path("/var/app/files")
content = safe_read_file("document.txt", BASE_DIR)
# Trying "../../etc/passwd" would be blocked
Limit File Uploads
from pathlib import Path
import mimetypes
ALLOWED_EXTENSIONS = {".txt", ".pdf", ".png", ".jpg"}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
def secure_upload(uploaded_file):
# Check file size
if uploaded_file.size > MAX_FILE_SIZE:
raise ValueError("File too large")
# Check extension
ext = Path(uploaded_file.filename).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("File type not allowed")
# Verify MIME type
mime_type, _ = mimetypes.guess_type(uploaded_file.filename)
if mime_type not in ["text/plain", "application/pdf", "image/png", "image/jpeg"]:
raise ValueError("Invalid file type")
# Save with a generated filename, never use user-provided names
import uuid
safe_name = f"{uuid.uuid4()}{ext}"
# ... save to storage
HTTPS and TLS
Always use encrypted connections in production.
SSL Context for Requests
import requests
# Verify certificates (always do this in production)
response = requests.get(
"https://api.example.com/data",
verify=True # Or path to CA bundle
)
# For systems with custom certificates
response = requests.get(
"https://internal-api/",
verify="/path/to/ca-bundle.crt"
)
Flask with HTTPS
from flask import Flask
app = Flask(__name__)
# Force HTTPS in production
@app.before_request
def require_https():
if not request.is_secure:
url = request.url.replace("http://", "https://")
return redirect(url, 301)
Authentication Patterns
Implement authentication carefully. Use established libraries when possible.
JWT Token Handling
import jwt
import os
from datetime import datetime, timedelta
SECRET_KEY = os.environ.get("JWT_SECRET")
def create_token(user_id: int) -> str:
payload = {
"user_id": user_id,
"exp": datetime.utcnow() + timedelta(hours=24)
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def verify_token(token: str) -> dict:
try:
return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
raise ValueError("Token expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid token")
Rate Limiting
Protect against brute-force attacks with rate limiting:
from flask import Flask, request, jsonify
from flask_limiter import Limiter
app = Flask(__name__)
limiter = Limiter(app, key_func=get_remote_address)
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute") # 5 attempts per minute
def login():
# ... login logic
Dependency Security
Keep your dependencies secure. Vulnerabilities in libraries affect your application.
Check for Vulnerabilities
# Using pip-audit
pip-audit
# Using safety
safety check
Pin Dependencies
# pyproject.toml
[project]
dependencies = [
"flask==3.0.0",
"requests>=2.31.0,<3.0.0",
]
Use tools like Dependabot or Renovate to stay updated on security patches.
Cross-Site Scripting (XSS) Prevention
When rendering user content, always escape HTML.
In Jinja2 Templates
Jinja2 auto-escapes by default:
<!-- Safe — automatically escaped -->
<p>{{ user_comment }}</p>
<!-- Unsafe — only use when content is trusted -->
<p>{{ user_comment | safe }}</p>
In JSON Responses
from flask import jsonify
import json
# Automatically escapes HTML in string values
@app.route("/api/user")
def get_user():
return jsonify({
"username": "<script>alert(xss)</script>"
})
# Output: {"username":"<script>alert('xss')</script>"}
Security Headers
Add security headers to protect against common attacks.
With Flask
from flask import Flask
app = Flask(__name__)
@app.after_request
def add_security_headers(response):
# Prevent clickjacking
response.headers["X-Frame-Options"] = "DENY"
# Prevent XSS
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-XSS-Protection"] = "1; mode=block"
# Content Security Policy
response.headers["Content-Security-Policy"] = "default-src self"
# Referrer policy
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
CSRF Protection
Protect forms against cross-site request forgery.
With Flask-WTF
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField("username", validators=[DataRequired()])
password = PasswordField("password", validators=[DataRequired()])
@app.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
# CSRF token automatically validated
# ... login logic
return render_template("login.html", form=form)
Logging Security Events
Monitor for suspicious activity.
import logging
logger = logging.getLogger("security")
def log_failed_login(username: str, ip_address: str):
logger.warning(
"Failed login attempt",
extra={
"username": username,
"ip_address": ip_address,
"event": "failed_login"
}
)
def log_suspicious_activity(details: dict):
logger.error(
"Suspicious activity detected",
extra={**details, "event": "suspicious"}
)
Security Checklist
Before deploying:
- All user input is validated
- Passwords are hashed with bcrypt or argon2
- Secrets come from environment variables
- HTTPS is enforced in production
- Dependencies are audited for vulnerabilities
- Security headers are configured
- Rate limiting is enabled on authentication endpoints
- File uploads are restricted and validated
- SQL queries use parameterization
- Logs do not contain sensitive data
Security is an ongoing process. Review and update your security practices regularly.
See Also
- secrets-module — Generate cryptographically secure random values
- hashlib-module — Secure hashing algorithms
- python-dotenv — Managing configuration securely