Python Security
Write secure Python code: validate inputs, prevent SQL injection, handle secrets safely, and use hashlib and the secrets module.
Input Validation
Never trust data from outside your application boundary — HTTP requests, files, environment variables, database results from other systems.
from pydantic import BaseModel, EmailStr, field_validator, ValidationError
from typing import Annotated
from pydantic import Field
class UserRegistration(BaseModel):
username: Annotated[str, Field(min_length=3, max_length=50, pattern=r"^[a-zA-Z0-9_]+$")]
email: EmailStr
age: Annotated[int, Field(ge=13, le=120)]
password: Annotated[str, Field(min_length=8, max_length=128)]
@field_validator("username")
@classmethod
def username_not_reserved(cls, v):
reserved = {"admin", "root", "system", "null"}
if v.lower() in reserved:
raise ValueError("This username is reserved")
return v
try:
user = UserRegistration(
username="alice_99",
email="alice@example.com",
age=25,
password="correct-horse-battery-staple"
)
except ValidationError as e:
print(e.json())
Pydantic is the standard for this in Python. For pure stdlib:
import re
def validate_username(username: str) -> str:
if not isinstance(username, str):
raise TypeError("Username must be a string")
username = username.strip()
if not 3 <= len(username) <= 50:
raise ValueError("Username must be 3-50 characters")
if not re.match(r"^[a-zA-Z0-9_]+$", username):
raise ValueError("Username may only contain letters, numbers, and underscores")
return username
SQL Injection Prevention
Never interpolate user input into SQL strings.
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
# UNSAFE — SQL injection possible
user_input = "'; DROP TABLE users; --"
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'") # NEVER DO THIS
# SAFE — parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
# SAFE — with named parameters
cursor.execute(
"SELECT * FROM users WHERE name = :name AND active = :active",
{"name": user_input, "active": True}
)
With SQLAlchemy (ORM):
from sqlalchemy import select
from sqlalchemy.orm import Session
# SAFE — ORM builds parameterized queries
stmt = select(User).where(User.name == user_input)
with Session(engine) as session:
users = session.execute(stmt).scalars().all()
secrets Module
Use secrets for anything security-related: tokens, passwords, API keys, session IDs.
import secrets
import string
# Secure random token (URL-safe base64)
token = secrets.token_urlsafe(32)
# "lX9z4v8KqW1mNpR7cT2aE5jH0dF3bI6e..." (43 chars for 32 bytes)
# Hex token
hex_token = secrets.token_hex(16)
# "a3f8c2d1e4b7a091f2e3d4c5b6a7e8f9"
# Secure random bytes
raw = secrets.token_bytes(32)
# Cryptographically secure choice
alphabet = string.ascii_letters + string.digits + string.punctuation
password = "".join(secrets.choice(alphabet) for _ in range(24))
# Secure random integer in range [0, n)
n = secrets.randbelow(100)
# Compare in constant time (prevents timing attacks)
import hmac
def verify_token(provided: str, stored: str) -> bool:
return hmac.compare_digest(provided, stored)
Password Hashing
pip install bcrypt
import bcrypt
def hash_password(plain: str) -> bytes:
salt = bcrypt.gensalt(rounds=12) # cost factor
return bcrypt.hashpw(plain.encode(), salt)
def verify_password(plain: str, hashed: bytes) -> bool:
return bcrypt.checkpw(plain.encode(), hashed)
# Store only the hash, never the plain password
hashed = hash_password("correct-horse-battery-staple")
verify_password("correct-horse-battery-staple", hashed) # True
verify_password("wrong-password", hashed) # False
For Argon2 (stronger, recommended for new systems):
pip install argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash("correct-horse-battery-staple")
ph.verify(hashed, "correct-horse-battery-staple") # True
hashlib: General-Purpose Hashing
import hashlib
# SHA-256 for file integrity / checksums
data = b"important data"
digest = hashlib.sha256(data).hexdigest()
# "c1ab7c4f9e3a2b5d8e0f1c2a3b4d5e6f..."
# File checksum
def file_sha256(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
# HMAC for message authentication
import hmac
secret_key = b"my-secret-key"
message = b"data to authenticate"
mac = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
# Verify — constant-time comparison
def verify_hmac(key: bytes, message: bytes, provided_mac: str) -> bool:
expected = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided_mac)
Environment Variables for Secrets
Never hardcode secrets in source code.
import os
from dotenv import load_dotenv # pip install python-dotenv
load_dotenv() # reads .env file into os.environ
DATABASE_URL = os.environ["DATABASE_URL"] # raises if missing
API_KEY = os.environ.get("API_KEY", "") # returns "" if missing
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY environment variable is required")
.env file (never commit to git):
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=your-random-secret-here
API_KEY=sk-abc123
.gitignore:
.env
*.env
.env.*
!.env.example
Preventing Path Traversal
import pathlib
BASE_DIR = pathlib.Path("/var/app/uploads").resolve()
def safe_open(filename: str):
# Resolve symlinks and normalize ../ sequences
target = (BASE_DIR / filename).resolve()
# Ensure the resolved path is within the allowed directory
if not target.is_relative_to(BASE_DIR):
raise PermissionError(f"Access denied: {filename}")
return open(target)
# Attacker tries: ../etc/passwd
safe_open("../etc/passwd") # raises PermissionError
Subprocess Safety
import subprocess
import shlex
# UNSAFE — shell=True with user input is command injection
user_filename = "file; rm -rf /"
subprocess.run(f"cat {user_filename}", shell=True) # DANGEROUS
# SAFE — pass as list, shell=False (default)
subprocess.run(["cat", user_filename], shell=False)
# SAFE — capture output, no shell
result = subprocess.run(
["git", "log", "--oneline", "-10"],
capture_output=True,
text=True,
check=True, # raises CalledProcessError on non-zero exit
timeout=30 # don't let it hang forever
)
print(result.stdout)
Common Security Checklist
- Use parameterized queries — never string-format SQL
- Hash passwords with bcrypt or argon2, never MD5/SHA1
- Use
secretsfor tokens, session IDs, and random choices - Load secrets from environment variables or a secrets manager
- Validate and sanitize all external input with Pydantic or explicit checks
- Use
hmac.compare_digestfor token comparison (timing-safe) - Pass commands as lists to
subprocess.run— avoidshell=True - Resolve and validate file paths before opening user-supplied names
- Pin dependencies and audit them with
pip audit
Frequently Asked Questions
How do I store passwords in Python?
Never store plain-text or MD5/SHA1 passwords. Use bcrypt, argon2-cffi, or passlib with a strong hashing algorithm designed for passwords — they include salting and are deliberately slow.
What's the difference between secrets and random?
random is for simulations and games — it uses a predictable PRNG. secrets uses the OS's cryptographically secure random source and is required for tokens, passwords, and keys.
How do I prevent SQL injection in Python?
Always use parameterized queries or an ORM. Never format user input directly into a SQL string.