Skip to content
Happy Programming Guide
Start learning
Python

Python Security

The security work that matters in Python: SQL injection, pickle, subprocess, secrets, dependency scanning and the randomness mistake.

A row of padlocks on a fence

Python makes several unsafe things easy, and a few of them look completely innocent. This guide covers the mistakes that cause real vulnerabilities — SQL injection, unpickling untrusted data, shell commands, weak randomness, path traversal and vulnerable dependencies — each with the fix.

1. SQL injection#

Python
# Dangerous
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
cursor.execute("SELECT * FROM users WHERE email = '%s'" % email)

An email of ' OR '1'='1 returns every row. Use placeholders, which send the query and the data separately:

Python
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))        # sqlite3
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))       # psycopg, mysql

The trailing comma matters — (email) is just a string in brackets, while (email,) is a one-element tuple.

With SQLAlchemy, use bound parameters rather than string building:

Python
from sqlalchemy import text

session.execute(
    text("SELECT * FROM users WHERE email = :email"),
    {"email": email},
)

2. pickle#

Python
import pickle

data = pickle.loads(request.data)      # remote code execution

Pickle is not a data format — it is a small program that reconstructs objects, and it can be made to call arbitrary functions during loading. Loading a pickle from an untrusted source is equivalent to running whatever code the sender chose.

Python
import json

data = json.loads(request.data)        # safe - data only

The same applies to yaml.load, which can construct arbitrary Python objects:

Python
import yaml

config = yaml.load(text)               # unsafe
config = yaml.safe_load(text)          # safe

And to any model or array file that uses pickle underneath — joblib files, older NumPy saves with allow_pickle=True, and some machine learning checkpoints. Treat a downloaded model file with the same caution as a downloaded executable.

3. Running shell commands#

Python
import subprocess

# Dangerous - a shell interprets the whole string
subprocess.run(f"convert {filename} out.png", shell=True)

A filename of a.png; rm -rf / runs both commands. Pass a list instead, which skips the shell entirely:

Python
subprocess.run(["convert", filename, "out.png"], check=True)

Arguments are passed directly to the program, so shell metacharacters have no special meaning. Add check=True so a failure raises rather than being ignored.

os.system() always uses a shell and has no safe form. Replace it with subprocess.run.

4. Randomness#

Python
import random

token = "".join(random.choices(string.ascii_letters, k=32))   # predictable

The random module uses a deterministic generator. Given enough output, its future values can be predicted — fine for shuffling a deck, unsafe for anything granting access.

Python
import secrets

token = secrets.token_urlsafe(32)
hex_token = secrets.token_hex(16)
password = "".join(secrets.choice(alphabet) for _ in range(16))

Use secrets for session identifiers, password reset tokens, API keys and one-time codes. It also provides compare_digest, which compares in constant time:

Python
if secrets.compare_digest(provided_token, expected_token):
    ...

5. Passwords#

Python
import hashlib

hashed = hashlib.md5(password.encode()).hexdigest()      # never
hashed = hashlib.sha256(password.encode()).hexdigest()   # also never

Both are designed to be fast, which is exactly wrong here. Use an algorithm designed to be slow:

Terminal
python -m pip install argon2-cffi
Python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

hasher = PasswordHasher()

stored = hasher.hash(password)          # salt included in the output

try:
    hasher.verify(stored, attempt)
except VerifyMismatchError:
    pass                                 # wrong password

bcrypt is an equally acceptable choice. Whichever you use, size the database column generously — a truncated hash fails verification for every user and the cause is not obvious from the symptom.

6. Path traversal#

Python
from pathlib import Path

# Dangerous
content = (Path("uploads") / request.args["name"]).read_text()

A name of ../../.env escapes the folder. Resolve and verify:

Python
base = Path("uploads").resolve()
target = (base / Path(name).name).resolve()

if not target.is_relative_to(base):
    abort(404)

content = target.read_text()

Two defences stacked: Path(name).name strips any directory part, and is_relative_to confirms the resolved path is still inside the base folder.

7. Secrets#

Python
import os

# Never in source
API_KEY = "sk-1234567890abcdef"

# From the environment
API_KEY = os.environ["API_KEY"]                       # required, fails loudly
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"

Using square brackets for required values means the program fails at start-up with a clear message rather than making a request with an empty key and getting a confusing 401.

Once a secret is in git history, rotating it is the only real fix. Add .env to .gitignore from the first commit, and consider a pre-commit hook that scans for key-shaped strings.

8. Dependencies#

Terminal
python -m pip install pip-audit
pip-audit

# Or
python -m pip install safety
safety check

Run one of these in your build pipeline so a new advisory fails the build rather than waiting to be noticed. Pin versions in requirements.txt so installs are reproducible, and check package names carefully — typosquatting on PyPI is a real and recurring attack.

9. Deserialising and parsing other formats#

Python
import xml.etree.ElementTree as ET

tree = ET.parse(untrusted_file)      # vulnerable to entity attacks
Terminal
python -m pip install defusedxml
Python
from defusedxml.ElementTree import parse

tree = parse(untrusted_file)         # entity expansion disabled

The standard library XML parsers are vulnerable to entity expansion attacks that can consume all available memory or read local files. The defusedxml package is a drop-in replacement.

10. eval and exec#

Python
result = eval(user_input)            # arbitrary code execution
exec(user_input)                     # the same

There is no safe way to sandbox these. Restricting the globals dictionary does not work — the escapes are well documented. For evaluating arithmetic, use ast.literal_eval, which only handles literals:

Python
import ast

value = ast.literal_eval("[1, 2, {'a': 3}]")     # safe
value = ast.literal_eval("__import__('os')")     # raises ValueError

A checklist#

  • Every query uses placeholders, including in admin scripts
  • No pickle.load or yaml.load on data you did not create
  • No shell=True with any user-supplied content
  • Tokens from secrets, never random
  • Passwords hashed with Argon2 or bcrypt
  • File paths resolved and checked against a base directory
  • Secrets from the environment, .env in .gitignore
  • A dependency audit in the build
  • Debug mode off in production

Questions people ask#

Is Python less secure than other languages?

No, but it makes several unsafe things convenient — pickle, eval, shell=True. The vulnerabilities come from convenience, not from the language design.

Is pickle ever acceptable?

Yes, for data you created and control, staying within your own system — a local cache, for instance. Never across a network boundary or from a download.

Do I need a framework for security?

Django and Flask with sensible extensions give you CSRF protection, escaping in templates and secure session handling by default. Writing that yourself means getting every detail right.

How do I keep dependencies current?

Pin versions, run an audit in your build, and let Dependabot or Renovate open the update pull requests. Being years behind is far harder to secure than staying close to current.

Where to go next#

Python best practices, including error handling and secretsRead next

Keep reading

Python

Python and Data Science

The four libraries that matter — NumPy, pandas, matplotlib and scikit-learn — and a complete first analysis from loading a CSV to…

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *