| name | python-security-guide |
| description | Reference knowledge base for the python-security-audit agent. Loaded by that agent on its first iteration when the host has not already injected it; not intended for direct invocation. |
Python Security: Comprehensive Guide to Identifying and Patching Vulnerabilities
Table of Contents
- Introduction
- Security Tools and Analysis
- Common Vulnerabilities
- Insecure Deserialization
- Web Framework Security
- Cryptography Best Practices
- Dependency Security and Supply Chain
- Secrets Management
- Security Checklist
Introduction
Python is one of the most widely used programming languages, powering web applications, data pipelines, APIs, and automation scripts. Despite Python's readability and productivity benefits, security vulnerabilities can arise from misuse of standard library functions, insecure third-party dependencies, or unsafe coding patterns.
Key Security Principles
- Never trust user-controlled input — validate and sanitize at every boundary
- Avoid dangerous built-ins:
eval, exec, pickle, yaml.load with untrusted data
- Use the
secrets module for security-sensitive random values, never random
- Keep dependencies pinned and audited (requirements.txt with hashes, pip-audit, safety)
- Prefer parameterized queries over string interpolation in SQL
- Run
bandit and pip-audit as part of CI/CD
Security Tools and Analysis
1. Bandit - Static Security Analysis
Bandit is the standard static analysis tool for Python security. It parses AST nodes and matches them against known-insecure patterns, assigning severity and confidence levels.
Installation:
pip install bandit
Usage:
bandit -r . -f txt
bandit -r . -f json -o bandit-report.json
bandit -r . -t B301,B302,B303
bandit -r . --exclude ./tests,./venv
Key Test IDs:
- B101:
assert used as security control
- B102:
exec usage
- B103: Setting permissive file permissions
- B104: Binding to all interfaces (0.0.0.0)
- B105/B106/B107: Hardcoded passwords
- B201: Flask debug=True
- B301/B302: Pickle/marshal deserialization
- B303/B304: MD5/SHA1 for security use
- B305: Cipher feedback mode
- B307:
eval usage
- B320: XML lxml library usage
- B324: Use of hashlib with insecure algorithm
- B501/B502: SSL/TLS misconfigurations
- B601/B602: Shell injection via subprocess
- B608: Hardcoded SQL expressions
- B701: Jinja2 autoescape disabled
- B702: Use of Mako templates
Resources:
2. pip-audit - Dependency Vulnerability Scanning
pip-audit scans installed packages against the PyPA Advisory Database and OSV database.
Installation:
pip install pip-audit
Usage:
pip-audit
pip-audit -r requirements.txt
pip-audit -r requirements.txt -f json -o audit.json
pip-audit --fix
Resources:
3. Safety - Dependency Vulnerability Database
Safety checks Python dependencies against a curated database of known vulnerabilities.
Installation:
pip install safety
Usage:
safety check
safety check -r requirements.txt
safety check --json
4. Semgrep - Advanced SAST
Semgrep provides pattern-based static analysis with community-maintained Python security rules.
Installation:
pip install semgrep
Usage:
semgrep --config=p/python-security .
semgrep --config=p/owasp-top-ten .
semgrep --config=p/r2c-security-audit .
Resources:
5. mypy - Type Checking for Security
Strong typing can prevent entire categories of security bugs related to type confusion.
pip install mypy
mypy --strict src/
Common Vulnerabilities
1. Injection Attacks
SQL Injection
Vulnerability:
String interpolation or f-strings used to construct SQL queries allow attackers to manipulate query structure and execute arbitrary SQL.
Vulnerable Code:
def get_user(db, username):
query = f"SELECT * FROM users WHERE username='{username}'"
db.execute(query)
query = "SELECT * FROM users WHERE username='" + username + "'"
db.execute(query)
query = "SELECT * FROM users WHERE username='%s'" % username
db.execute(query)
Secure Code:
def get_user(db, username):
query = "SELECT * FROM users WHERE username = %s"
db.execute(query, (username,))
db.execute("SELECT * FROM users WHERE username = ?", (username,))
from sqlalchemy import select
stmt = select(User).where(User.username == username)
result = session.execute(stmt)
from sqlalchemy import text
stmt = text("SELECT * FROM users WHERE username = :name")
result = db.execute(stmt, {"name": username})
Detection: bandit -t B608
Resources:
Command Injection
Vulnerability:
User-controlled input passed to shell commands allows arbitrary command execution.
Vulnerable Code:
import os
import subprocess
def run_ping(host):
os.system(f"ping -c 4 {host}")
def run_ping(host):
subprocess.call(f"ping -c 4 {host}", shell=True)
def run_command(cmd):
os.popen(cmd)
Secure Code:
import subprocess
import shlex
def run_ping(host):
import re
if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
raise ValueError("Invalid hostname")
subprocess.run(["ping", "-c", "4", host], check=True, capture_output=True)
def run_safe(user_input):
safe_input = shlex.quote(user_input)
subprocess.run(f"echo {safe_input}", shell=True, check=True)
Detection: bandit -t B601,B602,B605,B606,B607
Server-Side Template Injection (SSTI)
Vulnerability:
User input rendered directly in templates allows template engine code execution.
Vulnerable Code:
from jinja2 import Environment
def render_greeting(name):
env = Environment()
template = env.from_string(f"Hello {name}!")
return template.render()
@app.route('/greet')
def greet():
name = request.args.get('name')
return render_template_string(f"<h1>Hello {name}</h1>")
Secure Code:
from jinja2 import Environment, select_autoescape
def render_greeting(name):
env = Environment(autoescape=select_autoescape(['html', 'xml']))
template = env.from_string("Hello {{ name }}!")
return template.render(name=name)
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template('greet.html', name=name)
Detection: semgrep --config=p/python-security (rule: python.flask.security.injection.tainted-template-string)
2. Cross-Site Scripting (XSS)
Vulnerability:
User content rendered in HTML responses without escaping allows script injection.
Vulnerable Code:
from flask import Markup
@app.route('/user')
def show_user():
name = request.args.get('name')
return Markup(f"<p>Hello {name}</p>")
from django.utils.safestring import mark_safe
def view(request):
name = request.GET.get('name')
return HttpResponse(mark_safe(f"<p>{name}</p>"))
env = Environment(autoescape=False)
Secure Code:
from markupsafe import escape
@app.route('/user')
def show_user():
name = request.args.get('name', '')
safe_name = escape(name)
return f"<p>Hello {safe_name}</p>"
@app.route('/user')
def show_user():
name = request.args.get('name', '')
return render_template('user.html', name=name)
def view(request):
name = request.GET.get('name', '')
return render(request, 'user.html', {'name': name})
3. Path Traversal
Vulnerability:
User-controlled filenames used in file operations allow reading/writing arbitrary files.
Vulnerable Code:
import os
def read_user_file(filename):
path = "/var/www/uploads/" + filename
with open(path) as f:
return f.read()
@app.route('/download')
def download():
filename = request.args.get('file')
return send_file(f"/uploads/{filename}")
Secure Code:
import os
from pathlib import Path
BASE_DIR = Path("/var/www/uploads").resolve()
def read_user_file(filename):
requested = (BASE_DIR / filename).resolve()
if not str(requested).startswith(str(BASE_DIR) + os.sep):
raise PermissionError("Access denied: path traversal detected")
with open(requested) as f:
return f.read()
from flask import send_from_directory
@app.route('/download/<filename>')
def download(filename):
return send_from_directory("/uploads", filename)
Detection: bandit -t B102 and manual review
Insecure Deserialization
This is one of Python's most dangerous vulnerability classes. Several built-in modules can execute arbitrary code during deserialization of untrusted data.
pickle / cPickle
Vulnerability:
pickle.loads() executes arbitrary Python bytecode. Never deserialize untrusted pickle data.
Vulnerable Code:
import pickle
def load_session(data):
return pickle.loads(data)
@app.route('/load')
def load():
session_data = request.cookies.get('session')
import base64
obj = pickle.loads(base64.b64decode(session_data))
Secure Code:
import json
def load_session(data):
return json.loads(data)
import marshmallow
class UserSchema(marshmallow.Schema):
id = marshmallow.fields.Int()
name = marshmallow.fields.Str()
schema = UserSchema()
user = schema.loads(json_data)
import base64
import hmac
import hashlib
import os
import pickle
SECRET = os.environ['PICKLE_HMAC_SECRET'].encode()
def safe_dumps(obj):
data = pickle.dumps(obj)
sig = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return sig + ":" + base64.b64encode(data).decode()
def safe_loads(payload):
sig, _, encoded = payload.partition(":")
data = base64.b64decode(encoded)
expected = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
raise ValueError("Invalid signature")
return pickle.loads(data)
Detection: bandit -t B301,B302
yaml.load()
Vulnerability:
yaml.load() without an explicit Loader can deserialize Python objects and execute code.
Vulnerable Code:
import yaml
config = yaml.load(user_input)
config = yaml.load(user_input, Loader=yaml.FullLoader)
config = yaml.load(user_input, Loader=yaml.UnsafeLoader)
Secure Code:
import yaml
config = yaml.safe_load(user_input)
config = yaml.load(user_input, Loader=yaml.SafeLoader)
from pydantic import BaseModel
class Config(BaseModel):
host: str
port: int
raw = yaml.safe_load(config_text)
config = Config(**raw)
Detection: bandit -t B506
eval() and exec()
Vulnerability:
eval() and exec() with user input execute arbitrary Python code.
Vulnerable Code:
def calculate(expression):
return eval(expression)
def run_script(code):
exec(code)
Secure Code:
import ast
import operator
def calculate(expression):
try:
tree = ast.parse(expression, mode='eval')
return SafeEvalVisitor().visit(tree)
except Exception:
raise ValueError("Invalid expression")
import numexpr
result = numexpr.evaluate(expression)
config = ast.literal_eval(config_string)
Detection: bandit -t B307
Web Framework Security
Django
Common Issues:
DEBUG = True
SECRET_KEY = "hardcoded-secret-key"
ALLOWED_HOSTS = ['*']
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
Secure Django Settings:
import os
from pathlib import Path
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ['DJANGO_ALLOWED_HOSTS'].split(',')
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
CSRF Protection:
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
...
]
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def api_endpoint(request):
...
Flask
Vulnerable Flask App:
from flask import Flask
app = Flask(__name__)
app.secret_key = "dev"
app.debug = True
Secure Flask App:
import os
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
app.secret_key = os.environ['FLASK_SECRET_KEY']
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = 3600
csp = {
'default-src': "'self'",
'script-src': "'self'",
}
Talisman(app, content_security_policy=csp, force_https=True)
if __name__ == '__main__':
app.run(debug=os.environ.get('FLASK_DEBUG', 'False') == 'True')
FastAPI
Vulnerable FastAPI:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Secure FastAPI:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
app = FastAPI()
ALLOWED_ORIGINS = os.environ['ALLOWED_ORIGINS'].split(',')
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
security = HTTPBearer()
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
if not validate_jwt(token):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return token
@app.get("/protected")
async def protected_route(token: str = Depends(verify_token)):
return {"message": "authenticated"}
Cryptography Best Practices
Password Hashing
Vulnerable Code:
import hashlib
def hash_password(password):
return hashlib.md5(password.encode()).hexdigest()
def hash_password(password):
return hashlib.sha1(password.encode()).hexdigest()
def hash_password(password):
salt = "static_salt"
return hashlib.sha256((salt + password).encode()).hexdigest()
Secure Code:
import bcrypt
import os
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
def hash_password(password: str) -> str:
return ph.hash(password)
def verify_password(password: str, hashed: str) -> bool:
try:
return ph.verify(hashed, password)
except VerifyMismatchError:
return False
import hashlib, os
def hash_password(password: str) -> str:
salt = os.urandom(16)
hashed = hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1)
return salt.hex() + ":" + hashed.hex()
Secure Random Numbers
Vulnerable Code:
import random
token = random.randint(100000, 999999)
token = str(random.random())
session_id = ''.join(random.choices('abcdef', k=32))
Secure Code:
import secrets
token = secrets.token_hex(32)
token = secrets.token_urlsafe(32)
otp = secrets.randbelow(900000) + 100000
if secrets.compare_digest(user_token, expected_token):
pass
Encryption
Vulnerable Code:
from Crypto.Cipher import AES
key = b"hardcoded_key_16"
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(plaintext)
Secure Code:
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = Fernet.generate_key()
fernet = Fernet(key)
ciphertext = fernet.encrypt(plaintext.encode())
plaintext = fernet.decrypt(ciphertext).decode()
key = os.urandom(32)
nonce = os.urandom(12)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
TLS / SSL
Vulnerable Code:
import requests, ssl
response = requests.get(url, verify=False)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
Secure Code:
import requests, ssl, certifi
response = requests.get(url)
response = requests.get(url, verify=certifi.where())
import httpx
response = httpx.get(url)
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
Dependency Security and Supply Chain
Pinning Dependencies
Vulnerable:
# requirements.txt - unpinned, vulnerable to supply chain attacks
requests
flask
django
Secure:
# requirements.txt - pinned with hashes
requests==2.31.0 \
--hash=sha256:58cd2187423839a1b5cb... \
--hash=sha256:942c5a758f98d790eae...
flask==3.0.0 \
--hash=sha256:...
Generate hashes:
pip-compile --generate-hashes requirements.in > requirements.txt
Auditing Dependencies
pip-audit -r requirements.txt
safety check -r requirements.txt
osv-scanner -r requirements.txt
Supply Chain Attack Prevention
[tool.pip]
index-url = "https://pypi.org/simple/"
trusted-host = "pypi.org"
Secrets Management
Vulnerable Patterns
API_KEY = "sk-abc123secret"
DB_PASSWORD = "SuperSecret123!"
JWT_SECRET = "my-jwt-secret"
import logging
logging.info(f"Connecting with password: {db_password}")
subprocess.run(["curl", "-H", f"Authorization: Bearer {token}", url])
Secure Secrets Handling
import os
from typing import Optional
def get_secret(key: str) -> str:
value = os.environ.get(key)
if not value:
raise RuntimeError(f"Required secret '{key}' not set in environment")
return value
API_KEY = get_secret('API_KEY')
DB_PASSWORD = get_secret('DB_PASSWORD')
from dotenv import load_dotenv
load_dotenv()
import boto3
client = boto3.client('secretsmanager', region_name='us-east-1')
secret = client.get_secret_value(SecretId='my-app/db-password')['SecretString']
import hvac
client = hvac.Client(url='https://vault.example.com', token=os.environ['VAULT_TOKEN'])
secret = client.secrets.kv.v2.read_secret_version(path='my-app/db')['data']['data']['password']
Protecting Secrets in Logs
import logging
import re
class SensitiveDataFilter(logging.Filter):
PATTERNS = [
(re.compile(r'password=\S+', re.I), 'password=***'),
(re.compile(r'token=\S+', re.I), 'token=***'),
(re.compile(r'Authorization:\s*Bearer\s+\S+', re.I), 'Authorization: Bearer ***'),
(re.compile(r'"password"\s*:\s*"[^"]*"', re.I), '"password": "***"'),
]
def filter(self, record):
for pattern, replacement in self.PATTERNS:
record.msg = pattern.sub(replacement, str(record.msg))
return True
logging.getLogger().addFilter(SensitiveDataFilter())
Security Checklist
General Security
Input Validation
Authentication and Session Management
Cryptography
Web Framework
Dependencies
Secrets Management
Additional Resources