Overview
Python security bugs cluster around trusting input, building queries from strings, deserializing untrusted bytes, running shell commands with user data, and shipping secrets in code. The fixes are mechanical. Dependency scanning lives in python-dependency-management.
Validate external input with Pydantic at the boundary
Parse HTTP bodies, environment variables, files, and queue messages with Pydantic v2 before they reach business logic, and never pass the raw dict further.
from typing import Annotated
from pydantic import BaseModel, EmailStr, Field
class CreateUser(BaseModel):
username: Annotated[str, Field(min_length=3, max_length=50, pattern=r"^[a-z0-9_]+$")]
email: EmailStr
age: Annotated[int, Field(gt=0)]
req = CreateUser.model_validate(raw) # raises ValidationError with field-level detailsEmailStr requires the separate email-validator package. fastapi runs this validation from type annotations and returns 422 on failure.
Parameterize every SQL value
Pass values as parameters, never through f-strings or %. ORMs parameterize through their query builders, and they are injectable again the moment you build a text() clause with string formatting; use bound parameters there too.
conn.execute("SELECT * FROM users WHERE name = ?", (name,))Never load untrusted data with pickle
pickle.loads executes code embedded in the bytes. Use json.loads or model_validate_json for data that crosses a trust boundary, and yaml.safe_load instead of yaml.load. Use pickle only for caches where you control both writer and reader.
Extract archives with a safe filter
Extracting a hostile tar can write outside the target directory. Pass filter="data" to tarfile.extractall. Python 3.14 made the default filter more restrictive; passing it explicitly gives the same behavior on 3.12 and 3.13.
with tarfile.open(path) as tar:
tar.extractall(dest, filter="data")Run subprocesses without a shell
Pass an argument list and leave shell=False (the default). subprocess.run(f"convert {name}", shell=True) lets a filename become a command; subprocess.run(["convert", name], check=True) does not.
Load secrets from the environment and fail fast
Keep keys out of source, committed config, and image layers. Use os.environ["KEY"] for required values so a missing secret fails at startup, and .get() only for optional ones. Add .env to .gitignore, load it with python-dotenv in development only, and use the platform’s secret store in production. See secrets-and-env.
Defend outbound requests against SSRF
When the server fetches a user-supplied URL, check the address the hostname resolves to, not the hostname string. A blocklist of names misses decimal and hex IP forms, internal DNS names, and redirects.
import ipaddress, socket
from urllib.parse import urlparse
def assert_public(url: str) -> None:
host = urlparse(url).hostname
if urlparse(url).scheme != "https" or not host:
raise ValueError("https URL required")
for info in socket.getaddrinfo(host, None):
if not ipaddress.ip_address(info[4][0]).is_global:
raise ValueError("non-public address")Then fetch with follow_redirects=False, or re-run the check on every redirect target. DNS can change between the check and the connection, so also block internal ranges and the cloud metadata address at the network layer (egress rules or a proxy).