
Python Security
- 4 installs
- 21 repo stars
- Updated July 31, 2026
- jim60105/copilot-prompt
Design, review, and harden Python applications against the OWASP Top 10, covering injection, deserialization, SSRF, secrets, and static analysis with bandit/semgrep.
About
A structured guide for building secure Python apps across threat modeling, secure coding patterns, and verification against the OWASP Top 10. A developer uses it to audit Python code, harden security features, or set up security testing.
- Trust-boundary and data-flow threat modeling before coding
- Maps entry points to OWASP categories with bandit/safety/semgrep tooling
Python Security by the numbers
- 4 all-time installs (skills.sh)
- Ranked #1,738 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jim60105/copilot-prompt --skill python-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 21 |
| Last updated | July 31, 2026 |
| Repository | jim60105/copilot-prompt ↗ |
What it does
Design, review, and harden Python applications against the OWASP Top 10, covering injection, deserialization, SSRF, secrets, and static analysis with bandit/semgrep.
Files
Python Security Development Guide
Provide a structured approach to building secure Python applications, covering the OWASP Top 10, secure coding patterns, and verification checklists. Apply these guidelines throughout the secure development lifecycle — from threat modeling through deployment.
Secure Development Lifecycle
Phase 1: Threat Modeling and Secure Design
Before writing code, identify and mitigate threats at the design level:
- Identify trust boundaries — Map where untrusted data enters the system (HTTP requests, file uploads, database reads, environment variables, third-party APIs)
- Map data flows — Trace sensitive data (credentials, PII, tokens) through the system and verify protection at each stage
- Enumerate entry points — List all routes, endpoints, CLI arguments, message queue consumers, and cron jobs
- Map attack surfaces to OWASP Top 10 — Cross-reference each entry point against the OWASP categories in the quick reference table below
Design with security controls built-in:
- Centralized authentication and authorization middleware — never scatter auth checks across handlers
- Input validation at every trust boundary — validate early, reject invalid data before processing
- Least-privilege database access — use read-only connections where writes are not needed
- Defense in depth — layer multiple controls (input validation + parameterized queries + WAF)
- Fail securely — deny by default, require explicit grants
Phase 2: Secure Implementation
Critical Prohibitions
Never use these patterns. Violations are high-severity findings in any review.
| Never | Instead |
|---|---|
eval() / exec() with untrusted input | ast.literal_eval() or a dedicated parser |
pickle.load() with untrusted data | json.loads() or validated schema (e.g., Pydantic) |
yaml.load() | yaml.safe_load() |
shell=True + user input in subprocess | subprocess.run([cmd, arg1, arg2]) with list args |
os.system() | subprocess.run() |
| String formatting / f-strings in SQL | Parameterized queries (cursor.execute(sql, params)) |
random module for security purposes | secrets module |
| MD5 / SHA1 for password hashing | bcrypt or argon2-cffi |
assert for security checks | if not condition: raise SecurityError(...) |
Bare except: or except Exception: | except SpecificException: with proper handling |
| Hardcoded secrets in source code | Environment variables or secret manager (Vault, AWS SM) |
DEBUG=True in production | Environment-specific configuration |
Secure Implementation References
- For OWASP Top 10 details with vulnerable → secure code examples: See references/owasp-top-10.md
- For secure coding patterns organized by domain (input validation, auth, crypto, serialization, subprocess, file I/O, web frameworks): See references/secure-coding.md
Phase 3: Security Verification
Apply a layered verification approach:
1. Static Analysis — Detect common vulnerability patterns automatically
bandit— Python-specific security linter (AST-based)semgrep— Pattern-based analysis with OWASP and Python rulesetspylint— General linting with some security-relevant checks
2. Dependency Audit — Identify known vulnerabilities in third-party packages
pip-audit— Check installed packages against the OSV databasesafety— Check against the Safety vulnerability database
3. Secrets Detection — Find leaked credentials and API keys
detect-secrets— Baseline-aware secrets scanner
4. Code Review — Apply the security review workflow and checklists 5. Security Testing — Write negative tests that verify rejection of malicious inputs; fuzz-test parsers and validators
Quick tool commands:
# Bandit — static analysis
bandit -r src/ -f json -o bandit-report.json
# pip-audit — dependency vulnerabilities
pip-audit
# Safety — alternative dependency check
safety check
# detect-secrets — secrets scanning
detect-secrets scan > .secrets.baseline
# Semgrep — advanced pattern matching
semgrep --config=p/python --config=p/owasp-top-ten src/For complete verification checklists (code review, architecture review, dependency audit, deployment, testing, incident response): See references/security-checklist.md
Phase 4: Dependency and Deployment Security
Dependency Management
- Pin all dependencies with exact versions in
requirements.txt - Use hash verification:
pip install --require-hashes -r requirements.txt - Run
pip-auditin CI/CD pipeline on every build - Monitor for typosquatting — verify package names carefully before installing
- Review new dependencies before adding — check maintainership, download counts, known issues
Deployment Hardening
- Container security — Scan images with
trivy; use minimal base images (distroless, alpine); run as non-root user - HTTPS/TLS — Enforce TLS 1.2+ for all connections; redirect HTTP to HTTPS; set
Strict-Transport-Securityheader - Security headers — Configure
Content-Security-Policy,X-Content-Type-Options: nosniff,X-Frame-Options: DENY - Secrets at runtime — Inject secrets via environment variables or mounted volumes; never bake into images
- Least privilege — Run processes as non-root; use read-only filesystems where possible; limit network access
- Logging — Use structured logging (JSON); never log passwords, tokens, PII, or full stack traces to users; log authentication events and access denials for audit
OWASP Top 10:2025 Quick Reference
Map each OWASP 2025 category to Python-specific risks and primary mitigations:
| # | Category | Python-Specific Risks | Primary Mitigation |
|---|---|---|---|
| A01 | Broken Access Control | Missing @login_required / auth decorators, IDOR via sequential IDs, path traversal, SSRF via requests.get(user_url) | Centralized auth middleware, object-level permissions, pathlib.resolve(), URL allowlisting |
| A02 | Security Misconfiguration | DEBUG=True in prod, CORS(origins="*"), Swagger/docs exposed, default SECRET_KEY, XXE via xml.etree | Environment-specific config, explicit CORS origins, disable docs in prod, defusedxml |
| A03 | Software Supply Chain Failures | Unpinned deps, typosquatting, no SBOM, unvetted transitive deps, CI/CD secrets exposure | pip-audit in CI, pinned versions with hashes, SBOM generation, CI/CD hardening |
| A04 | Cryptographic Failures | random module for tokens, MD5/SHA1 password hashing, hardcoded API keys, no encryption at rest | secrets module, bcrypt/argon2, env vars / secret manager, cryptography library |
| A05 | Injection | SQL via f-strings/.format(), shell=True, Jinja2 ` | safe / SSTI, eval()/exec()` |
| A06 | Insecure Design | No rate limiting, missing input validation layer, no abuse case modeling | Threat modeling, validation at boundaries (Pydantic), rate limiting middleware |
| A07 | Authentication Failures | Weak session config, JWT algorithm="none" or HS256 with public key, no brute-force protection | Secure session settings, explicit algorithms=["RS256"], account lockout / rate limiting |
| A08 | Software or Data Integrity Failures | pickle.loads() / yaml.load() deserialization, unsigned updates, CI/CD pipeline injection | json.loads() / yaml.safe_load(), signed artifacts, pinned CI actions with SHA |
| A09 | Security Logging and Alerting Failures | Logging passwords/tokens, no auth event logging, missing alerting, no playbooks | Structured logging with field filtering, audit trail, alerting thresholds, honeytokens |
| A10 | Mishandling of Exceptional Conditions | Bare except: pass, failing open, transaction rollback failures, sensitive info in errors | Specific exception types, context managers, centralized error handlers, fail-closed patterns |
For detailed vulnerable → secure code examples for each category: See references/owasp-top-10.md
Security Review Workflow
Follow this procedure when reviewing Python code for security:
1. Scan for critical prohibitions — Check for any pattern in the "Critical Prohibitions" table above. Each match is an immediate high-severity finding. 2. Check input validation — Verify every entry point (route handler, CLI argument, file parser, queue consumer) validates and sanitizes input before processing. 3. Verify authentication and authorization — Confirm every endpoint requires authentication (unless explicitly public) and checks authorization for the specific resource being accessed. 4. Review data handling — Trace how secrets, PII, and sensitive data flow through the system. Verify encryption at rest and in transit, proper key management, and secure deletion. 5. Check error handling — Ensure errors do not leak stack traces, internal paths, database details, or configuration to users. Verify fail-secure behavior. 6. Audit dependencies — Run pip-audit and safety check. Flag any unpinned dependencies or packages with known CVEs. 7. Verify logging — Confirm no sensitive data (passwords, tokens, PII) appears in logs. Verify authentication events, authorization failures, and security-relevant actions are logged. 8. Run static analysis — Execute bandit -r src/ and review findings. Run semgrep with Python and OWASP rulesets for deeper analysis. 9. Report findings — For each finding, document: severity (Critical/High/Medium/Low), location (file:line), vulnerable code snippet, explanation of the risk, and recommended fix with code example.
Security Hardening Quick Commands
# === Static Analysis ===
pip install bandit && bandit -r src/ -f json -o bandit-report.json
pip install semgrep && semgrep --config=p/python --config=p/owasp-top-ten src/
# === Dependency Audit ===
pip install pip-audit && pip-audit
pip install safety && safety check
# === Secrets Detection ===
pip install detect-secrets && detect-secrets scan > .secrets.baseline
# === Pin Dependencies with Hashes ===
pip install pip-tools && pip-compile --generate-hashes requirements.in
# === Container Scanning ===
# trivy image <image-name>Reference Files
Consult these files for detailed guidance beyond this overview:
- [references/owasp-top-10.md](references/owasp-top-10.md) — Detailed OWASP Top 10 coverage with Python-specific vulnerable → secure code examples for each category, including Django, Flask, and FastAPI patterns
- [references/secure-coding.md](references/secure-coding.md) — Secure coding patterns organized by domain: input validation, authentication, cryptography, serialization, subprocess execution, file operations, and web framework configuration (Django, Flask, FastAPI)
- [references/security-checklist.md](references/security-checklist.md) — Actionable verification checklists for code review, architecture review, dependency audit, deployment hardening, security testing, and incident response
OWASP Top 10:2025 — Python Security Reference
Reference for AI agents performing Python security reviews, threat modeling, and secure code generation.
Table of Contents
- A01: Broken Access Control
- A02: Security Misconfiguration
- A03: Software Supply Chain Failures
- A04: Cryptographic Failures
- A05: Injection
- A06: Insecure Design
- A07: Authentication Failures
- A08: Software or Data Integrity Failures
- A09: Security Logging and Alerting Failures
- A10: Mishandling of Exceptional Conditions
---
A01: Broken Access Control
Failure to enforce that users act only within their intended permissions. Remains the most common web application vulnerability. In 2025, SSRF (previously A10:2021) is consolidated here as a CWE under broken access control.
Python-Specific Risks
- Missing authorization decorators on views/endpoints
- Insecure Direct Object References (IDOR): accessing objects by user-supplied ID without ownership check
- Path traversal via unsanitized user input in file operations (
os.path.joinwith absolute user paths) - Relying solely on client-side or frontend checks
- Overly permissive CORS configuration
- Server-Side Request Forgery (SSRF): fetching user-supplied URLs without validation
Vulnerable Code
# IDOR — no ownership verification
@app.route("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = Order.query.get(order_id) # Any authenticated user can access any order
return jsonify(order.to_dict())
# Path traversal
@app.route("/files")
def get_file():
filename = request.args.get("name")
return send_file(os.path.join("/uploads", filename)) # ../../etc/passwd
# SSRF — user controls the URL entirely
@app.route("/fetch")
def fetch_url():
url = request.args.get("url")
response = requests.get(url) # Can reach http://169.254.169.254/metadata
return response.textSecure Code
# Object-level permission check
@app.route("/api/orders/<int:order_id>")
@login_required
def get_order(order_id):
order = Order.query.get_or_404(order_id)
if order.user_id != current_user.id:
abort(403)
return jsonify(order.to_dict())
# Safe file access with path confinement
@app.route("/files")
def get_file():
filename = request.args.get("name")
safe_path = Path("/uploads").resolve() / filename
if not safe_path.resolve().is_relative_to(Path("/uploads").resolve()):
abort(400)
return send_file(safe_path)
# SSRF protection — validate URL and resolved IP
from urllib.parse import urlparse
import ipaddress, socket
ALLOWED_SCHEMES = {"https"}
BLOCKED_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # Cloud metadata
ipaddress.ip_network("::1/128"),
]
def validate_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
if not parsed.hostname:
return False
try:
resolved_ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
except (socket.gaierror, ValueError):
return False
return all(resolved_ip not in net for net in BLOCKED_NETWORKS)
@app.route("/fetch")
def fetch_url():
url = request.args.get("url")
if not validate_url(url):
abort(400, "URL not allowed")
response = requests.get(url, timeout=5, allow_redirects=False)
return response.textMitigation Strategies
- Deny by default; require explicit authorization for every endpoint
- Enforce object-level permission checks (not just role checks)
- Use
pathlib.Path.resolve()andis_relative_to()to prevent path traversal - Return 404 (not 403) for unauthorized resources to prevent enumeration
- Log and alert on access control failures
- SSRF: validate URLs, block private/internal IPs and cloud metadata endpoints
- SSRF: resolve DNS and validate IP before requests; disable redirects or re-validate
---
A02: Security Misconfiguration
Insecure default configurations, incomplete setup, open cloud storage, misconfigured HTTP headers, verbose error messages, or XXE vulnerabilities. Moved up from #5 in 2021. XXE (XML External Entities) is now covered here.
Python-Specific Risks
- Django:
DEBUG=True, defaultSECRET_KEY, emptyALLOWED_HOSTS - Flask:
app.run(debug=True)in production, weak secret key - FastAPI: Swagger/ReDoc docs exposed in production, permissive CORS
- Exposed
__pycache__,.pyc,.env,requirements.txtvia static file serving - Default admin credentials left active
- Missing security headers (CSP, HSTS, X-Content-Type-Options)
- XML parsers with external entity processing enabled (XXE)
Vulnerable Configuration
# Django — INSECURE
DEBUG = True
SECRET_KEY = "django-insecure-abc123"
ALLOWED_HOSTS = ["*"]
# Flask — INSECURE
app.secret_key = "dev"
app.run(debug=True, host="0.0.0.0")
# XXE — INSECURE XML parsing
from lxml import etree
parser = etree.XMLParser(resolve_entities=True)
tree = etree.parse(user_upload, parser)Secure Configuration
# Django — SECURE
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["example.com", "www.example.com"]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
# Flask — SECURE
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"
# FastAPI — disable docs in production, restrictive CORS
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
# XXE — SECURE: use defusedxml or disable entities
from defusedxml import ElementTree
tree = ElementTree.parse(user_upload)Mitigation Strategies
- Disable debug modes, interactive docs, and verbose errors in production
- Set
ALLOWED_HOSTS/ CORS origins to explicit values — never*with credentials - Configure security headers via middleware (
django-csp,flask-talisman) - Use
defusedxmlfor all XML parsing; disable entity resolution inlxml - Run
python manage.py check --deployfor Django security checks
---
A03: Software Supply Chain Failures
Renamed and significantly expanded from "Vulnerable and Outdated Components" (A06:2021). Covers the entire software supply chain: known vulnerabilities, unpinned dependencies, typosquatting, transitive dependency risks, CI/CD pipeline security, SBOM management, vendor compromise, and malicious packages.
Python-Specific Risks
- Outdated packages with known CVEs in
requirements.txt - Unpinned dependencies pulling in vulnerable versions
- Typosquatting attacks on PyPI (e.g.,
python-nmapvsnmap) - Transitive dependency vulnerabilities not visible in direct deps
- No Software Bill of Materials (SBOM) for deployed applications
- CI/CD secrets exposed in logs or untrusted workflows
- Unvetted PyPI packages with embedded malicious code
- Lack of hash verification during package installation
Vulnerability Scanning
# pip-audit — recommended by PyPA
pip-audit
pip-audit -r requirements.txt
# safety (alternative scanner)
safety check
# Generate CycloneDX SBOM
pip-audit --format=cyclonedx-json -o sbom.jsonDependency Pinning and Hash Verification
# requirements.txt — pin exact versions with hashes
flask==3.0.0 \
--hash=sha256:...
requests==2.31.0 \
--hash=sha256:...
# Generate pinned requirements with hashes using pip-compile
pip install pip-tools
pip-compile --generate-hashes requirements.in -o requirements.txt
# Install with hash verification
pip install --require-hashes -r requirements.txtTyposquatting Prevention
Verify package names before installing: check official docs, download stats, maintainer reputation, and source repository. Common typosquat targets: python-dateutil vs python3-dateutil, python-nmap vs nmap, urllib3 vs urllib, beautifulsoup4 vs beautifulsoup.
CI/CD Pipeline Security
- Pin GitHub Actions by commit SHA, not tags
- Never expose secrets in logs; use
::add-mask::in GitHub Actions - Use
pull_request(notpull_request_target) for untrusted code - Restrict CI/CD secrets to specific branches and environments
Mitigation Strategies
- Pin all dependencies to exact versions with hashes
- Use
pip-compile --generate-hashesand--require-hashesfor integrity verification - Run
pip-auditorsafetyin CI/CD to block vulnerable dependencies - Enable Dependabot or Renovate for automated dependency updates
- Generate and maintain SBOM (CycloneDX or SPDX format)
- Verify package names carefully — check download stats and maintainer on PyPI
- Audit transitive dependencies with
pipdeptree - Pin CI/CD actions by commit SHA, not mutable tags
- Use virtual environments and private PyPI mirrors for production
- Implement staged rollouts and change management for dependency updates
---
A04: Cryptographic Failures
Failure to properly protect data in transit and at rest, including use of weak algorithms or poor key management.
Python-Specific Risks
- Using
hashlib.md5()/hashlib.sha1()for password hashing - Using
randommodule for security-sensitive values (predictable PRNG) - Hardcoded secrets, API keys, or encryption keys in source code
- Weak TLS configuration or disabled certificate verification (
verify=False) - Storing secrets in
.envfiles committed to version control
Vulnerable Code
# Weak password hashing
password_hash = hashlib.md5(password.encode()).hexdigest()
# Predictable token generation
token = "".join(random.choices("abcdef0123456789", k=32))
# Hardcoded secret
SECRET_KEY = "super-secret-key-12345"
# Disabled TLS verification
response = requests.get(url, verify=False)Secure Code
import secrets
from argon2 import PasswordHasher
# Strong password hashing
ph = PasswordHasher()
password_hash = ph.hash(password)
# Cryptographically secure token
token = secrets.token_urlsafe(32)
# Load secret from environment
SECRET_KEY = os.environ["SECRET_KEY"] # Fail loudly if missingKey Management Patterns
- Load secrets from environment variables or a secrets manager
- Use
cryptographylibrary (notpycrypto) for encryption - Generate keys with
os.urandom()orsecrets.token_bytes() - Rotate secrets periodically; support multiple active keys during rotation
Mitigation Strategies
- Use
bcryptorargon2-cffifor password hashing — never raw hash functions - Use
secretsmodule for all security-sensitive random values - Enforce TLS 1.2+ and valid certificates; pin certificates for critical services
- Audit codebase for hardcoded secrets using tools like
detect-secretsortrufflehog - Classify data and apply encryption based on sensitivity
---
A05: Injection
Untrusted data sent to an interpreter as part of a command or query. Includes SQL, OS command, and template injection.
Python-Specific Risks
- Raw SQL string formatting with
cursor.execute(f"...") os.system()orsubprocesswithshell=Trueand user input- Jinja2 server-side template injection (SSTI) via
Template(user_input) - ORM escape hatches:
extra(),raw(),RawSQL()in Django eval(),exec(),compile()with user-controlled input
SQL Injection
# VULNERABLE — string formatting
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
cursor.execute("SELECT * FROM users WHERE name = '%s'" % name)
# SECURE — parameterized queries
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# SQLAlchemy — use bound parameters
db.session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
# Django ORM — safe by default, but watch for raw()
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id]) # Safe
User.objects.raw(f"SELECT * FROM users WHERE id = {user_id}") # VULNERABLECommand Injection
# VULNERABLE — shell=True with user input
subprocess.call(f"convert {filename} output.png", shell=True)
os.system(f"ping {host}")
# SECURE — list arguments, no shell
subprocess.run(["convert", filename, "output.png"], check=True)
# Use shlex.quote() when shell is unavoidable
import shlex
subprocess.run(f"echo {shlex.quote(user_input)}", shell=True)Template Injection (SSTI)
# VULNERABLE — user input as template source
template = Template(user_input) # SSTI: user_input = "{{ config }}"
# SECURE — user input as template data only
template = Template("Hello, {{ name }}!")
output = template.render(name=user_input)Mitigation Strategies
- Use parameterized queries for ALL database operations — no exceptions
- Never use
shell=Truewith user input; pass arguments as lists - Never pass user input as template source — only as template data
- Never use
eval(),exec(), orcompile()with user-controlled input - Use ORM query builders; audit all uses of
raw(),extra(),RawSQL()
---
A06: Insecure Design
Missing or ineffective security controls at the design level. Unlike implementation bugs, insecure design cannot be fixed by perfect code alone. Moved from #4 to #6 in 2025.
Python-Specific Risks
- No rate limiting on authentication or sensitive endpoints
- Business logic flaws: missing quantity limits, price manipulation
- No account lockout after failed login attempts
- APIs returning more data than the client needs (over-fetching)
Vulnerable vs Secure Design
# VULNERABLE — no rate limiting, brute force possible
@app.route("/login", methods=["POST"])
def login():
user = User.query.filter_by(email=request.json["email"]).first()
if user and user.check_password(request.json["password"]):
return create_token(user)
return jsonify({"error": "Invalid credentials"}), 401
# SECURE — rate limited with explicit serialization
from flask_limiter import Limiter
limiter = Limiter(app, default_limits=["100 per hour"])
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
user = User.query.filter_by(email=request.json["email"]).first()
if user and user.check_password(request.json["password"]):
return create_token(user)
return jsonify({"error": "Invalid credentials"}), 401
# VULNERABLE — over-fetching
return jsonify(user.__dict__) # Exposes password hash, internal fields
# SECURE — explicit serialization
return jsonify({"id": user.id, "name": user.name, "email": user.email})Mitigation Strategies
- Implement rate limiting on all authentication and sensitive endpoints
- Validate business logic constraints server-side
- Use explicit serialization schemas (Pydantic, Marshmallow) — never serialize ORM objects directly
- Apply fail-secure defaults: deny on error, lock on suspicious activity
- Enforce account lockout or progressive delays after repeated failures
---
A07: Authentication Failures
Weaknesses in authentication mechanisms that allow attackers to compromise passwords, keys, or session tokens. Renamed from "Identification and Authentication Failures" in 2021.
Python-Specific Risks
- Session fixation: not regenerating session ID after login
- JWT misconfiguration:
algorithm="none", missing expiry, secret in source code - Storing passwords in plaintext or with reversible encryption
- Missing MFA on privileged accounts
- OAuth2 state parameter omission (CSRF in OAuth flow)
JWT Pitfalls
# VULNERABLE — algorithm confusion, no expiry
payload = jwt.decode(token, options={"verify_signature": False}) # DANGER
token = jwt.encode({"user_id": 1}, SECRET_KEY, algorithm="HS256") # No expiry
# SECURE — explicit algorithm, expiry, audience
from datetime import datetime, timedelta, timezone
token = jwt.encode(
{
"user_id": 1,
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"aud": "myapp",
},
os.environ["JWT_SECRET"],
algorithm="HS256",
)
payload = jwt.decode(
token,
os.environ["JWT_SECRET"],
algorithms=["HS256"], # Explicit list — prevent algorithm confusion
audience="myapp",
)Session Management
- Django:
login(request, user)automatically cycles session key - Flask: call
session.clear()before setting new session data after authentication - Set
session.permanent = Trueand configurePERMANENT_SESSION_LIFETIME
Mitigation Strategies
- Use established auth libraries (
Flask-Login,django.contrib.auth,FastAPIOAuth2 utilities) - Always specify
algorithms=["HS256"](list) when decoding JWTs — prevent algorithm confusion - Set JWT expiry (
exp) and validate it; use short-lived access tokens with refresh tokens - Store JWT secrets in environment variables — never in source code
- Regenerate session IDs after authentication
- Implement account lockout after repeated failed attempts
- Enforce MFA for admin and privileged accounts
- Validate OAuth2
stateparameter to prevent CSRF
---
A08: Software or Data Integrity Failures
Failure to protect against integrity violations: insecure deserialization, untrusted CI/CD pipelines, unsigned updates. Renamed from "Software and Data Integrity Failures" in 2021 (changed "and" to "or").
Python-Specific Risks
pickle.loads()on untrusted data — arbitrary code executionyaml.load()withoutSafeLoader— arbitrary code executionjsonpickle.decode()on untrusted input — code execution via object reconstruction- Unsigned packages or pip installs without hash verification
- CI/CD pipeline injection via unvalidated pull request triggers
Dangerous Deserialization
import pickle
import yaml
# VULNERABLE — arbitrary code execution
data = pickle.loads(untrusted_bytes) # RCE
data = yaml.load(untrusted_string) # RCE (PyYAML < 6.0 default)
data = yaml.load(untrusted_string, Loader=yaml.FullLoader) # Still risky
import jsonpickle
obj = jsonpickle.decode(untrusted_json) # RCE
import shelve
db = shelve.open("data.db") # Uses pickle internallySecure Alternatives
import json
import yaml
# SECURE — use safe formats and explicit schemas
data = json.loads(untrusted_string) # No code execution possible
data = yaml.safe_load(untrusted_string) # SafeLoader only
# Structured deserialization with Pydantic
from pydantic import BaseModel, EmailStr
class UserInput(BaseModel):
name: str
email: EmailStrMitigation Strategies
- Never use
pickle,shelve,marshal, orjsonpicklewith untrusted data - Always use
yaml.safe_load()— neveryaml.load()with untrusted input - Use
jsonfor data interchange; use Pydantic or Marshmallow for validation - Verify dependency integrity with hash pinning (
--require-hashes) - Sign artifacts and verify signatures in deployment pipelines
---
A09: Security Logging and Alerting Failures
Insufficient logging, alerting, and active response capabilities, preventing detection of and response to breaches and attacks. Renamed from "Security Logging and Monitoring Failures" in 2021 to emphasize active alerting, honeytokens, and incident playbooks over passive monitoring.
Python-Specific Risks
- Using
print()instead ofloggingmodule - Logging sensitive data: passwords, tokens, API keys, PII
- Log injection via unsanitized user input in log messages
- No logging of authentication events or access control failures
- Missing centralized log aggregation, alerting, and honeytokens
Logging Guidelines
Log: authentication events, authorization failures, input validation failures, system events, administrative actions.
Never log: passwords, session/JWT/API tokens, credit card numbers, SSNs, PII, or full request bodies with sensitive data.
Log Injection Prevention
import logging
# VULNERABLE — user input directly in log format string
logger.info(f"Login attempt for user: {username}")
# Attacker input: "admin\n2024-01-01 INFO Login successful for user: admin"
# SECURE — use logging parameters (processed by formatter, not string interpolation)
logger.info("Login attempt for user: %s", username)
# Sanitize for structured logging
def sanitize_log_value(value: str) -> str:
return value.replace("\n", "\\n").replace("\r", "\\r")
logger.info("Login attempt for user: %s", sanitize_log_value(username))Structured Logging and Alerting
# Structured JSON logging
import structlog
logger = structlog.get_logger()
logger.info("auth.login_success", user_id=user.id, ip=request.remote_addr)
# Alert on security events
def alert_on_brute_force(user_id: str, failed_count: int):
if failed_count >= 5:
logger.critical(
"security.brute_force_detected",
user_id=user_id,
failed_attempts=failed_count,
action="account_locked",
)Mitigation Strategies
- Use Python
loggingmodule with appropriate levels — never bareprint() - Log security events: authentication, authorization, validation failures
- Scrub sensitive data before logging; use allowlists for loggable fields
- Use parameterized logging (
%s) — avoid f-strings in log calls - Implement structured logging (JSON) for machine-parseable output
- Aggregate logs centrally; set up automated alerts for anomalous patterns
- Deploy honeytokens and canary values to detect breaches
- Define incident response playbooks for common security alert types
- Monitor for: repeated auth failures, privilege escalation, unusual access patterns
---
A10: Mishandling of Exceptional Conditions
New in 2025. Improper error handling, failing open instead of closed, uncaught exceptions, resource leaks, sensitive information in error messages, missing parameter handling, race conditions from error states, and transaction rollback failures. SSRF (previously A10:2021) moved to A01.
Python-Specific Risks
- Bare
except:orexcept Exception: passthat swallow errors silently - Failing open instead of closed when errors occur
- Not rolling back database transactions on error
- Resource leaks when exceptions bypass cleanup (files, connections, locks)
- Exposing tracebacks and internal details in production error responses
- Using
assertfor error handling (stripped bypython -O) - Race conditions introduced by partially completed operations on error
Related CWEs
CWE-209 (sensitive error messages), CWE-248 (uncaught exception), CWE-390 (error without action), CWE-396 (generic exception catch), CWE-636 (failing open).
Vulnerable Code — Swallowing Errors
# VULNERABLE — bare except swallows all errors including SystemExit, KeyboardInterrupt
try:
result = process_payment(order)
except:
pass # Payment failure silently ignored — order proceeds unpaid
# VULNERABLE — catching Exception too broadly
try:
user = authenticate(credentials)
except Exception:
user = AnonymousUser() # Failing OPEN — auth error grants access
# VULNERABLE — assert stripped by python -O
def withdraw(account, amount):
assert amount > 0, "Amount must be positive" # Removed in optimized mode
assert account.balance >= amount, "Insufficient funds"
account.balance -= amountSecure Code — Specific Exception Handling
# SECURE — catch specific exceptions, fail closed
try:
result = process_payment(order)
except PaymentDeclinedError:
logger.warning("Payment declined for order %s", order.id)
order.status = "payment_failed"
raise
except PaymentGatewayError as e:
logger.error("Payment gateway error for order %s: %s", order.id, e)
order.status = "payment_pending"
raise
# SECURE — fail closed on auth errors
try:
user = authenticate(credentials)
except AuthenticationError:
logger.warning("Authentication failed for %s", credentials.username)
raise HTTPException(status_code=401, detail="Authentication failed")
except Exception:
logger.exception("Unexpected error during authentication")
raise HTTPException(status_code=500, detail="Internal server error")
# SECURE — explicit validation instead of assert
def withdraw(account, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if account.balance < amount:
raise InsufficientFundsError(f"Balance {account.balance} < {amount}")
account.balance -= amountVulnerable Code — Resource Leaks and Transaction Failures
# VULNERABLE — resource leak on exception
def process_file(path):
f = open(path)
data = json.load(f) # If this raises, file handle leaks
f.close()
return data
# VULNERABLE — transaction not rolled back on error
def transfer_funds(from_acct, to_acct, amount):
from_acct.balance -= amount
db.session.flush()
# If next line raises, from_acct is debited but to_acct is not credited
to_acct.balance += amount
db.session.commit()
# VULNERABLE — sensitive info in error response
@app.errorhandler(Exception)
def handle_error(e):
return jsonify({
"error": str(e),
"traceback": traceback.format_exc(), # Exposes internals
"db_url": app.config["SQLALCHEMY_DATABASE_URI"], # Credential leak
}), 500Secure Code — Resource Management and Transactions
# SECURE — context manager ensures cleanup
def process_file(path):
with open(path) as f:
return json.load(f)
# SECURE — transaction with proper rollback
def transfer_funds(from_acct, to_acct, amount):
try:
from_acct.balance -= amount
to_acct.balance += amount
db.session.commit()
except Exception:
db.session.rollback()
logger.exception("Transfer failed: %s -> %s, amount=%s", from_acct.id, to_acct.id, amount)
raise
# Or use nested transaction context
def transfer_funds(from_acct, to_acct, amount):
with db.session.begin_nested():
from_acct.balance -= amount
to_acct.balance += amount
db.session.commit()
# SECURE — sanitized error response
@app.errorhandler(Exception)
def handle_error(e):
logger.exception("Unhandled exception") # Full details in server logs only
return jsonify({"error": "Internal server error"}), 500Global Exception Middleware
# FastAPI — centralized exception handling
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(
status_code=500,
content={"error": "Internal server error"},
)Mitigation Strategies
- Catch specific exceptions; fail closed on unexpected errors
- Use context managers (
withstatement) for all resource management - Roll back database transactions on error — use
try/except/rollbackorbegin_nested() - Implement centralized error handlers and global exception middleware
- Never expose tracebacks or internal details in production error responses
- Use explicit validation (
if/raise) instead ofassertfor error handling - Log full exception details server-side; return generic messages to clients
- Test error paths: verify that failures leave the system in a consistent state
Python Secure Coding Reference
Reference for AI agents implementing secure Python code. Use imperative patterns; prefer allowlisting, least privilege, and defense in depth.
Table of Contents
- 1. Input Validation and Sanitization
- 2. Authentication and Authorization Patterns
- 3. Cryptography Best Practices
- 4. Secure Data Handling
- 5. File and Path Operations
- 6. Subprocess and System Interaction
- 7. Serialization and Deserialization
- 8. Web Framework Security
- 9. Error Handling and Information Disclosure
- 10. Concurrency and Race Conditions
---
1. Input Validation and Sanitization
Validate all inputs at the boundary using allowlists and strict type coercion. Reject anything not explicitly permitted.
Allowlisting vs Denylisting
# ❌ Anti-pattern: denylisting dangerous characters
def sanitize(value: str) -> str:
for char in ["<", ">", "&", "'", '"']:
value = value.replace(char, "")
return value
# ✅ Correct: allowlist permitted characters
import re
def validate_username(value: str) -> str:
if not re.fullmatch(r"[a-zA-Z0-9_\-]{3,32}", value):
raise ValueError("Invalid username")
return valueType Coercion and Validation
# ❌ Anti-pattern: manual validation
def process(data: dict):
age = int(data.get("age", 0)) # no bounds, no type safety
# ✅ Correct: pydantic model with constraints
from pydantic import BaseModel, Field, EmailStr
class UserInput(BaseModel):
username: str = Field(min_length=3, max_length=32, pattern=r"^[a-zA-Z0-9_\-]+$")
age: int = Field(ge=0, le=150)
email: EmailStrReDoS Prevention
# ❌ Anti-pattern: catastrophic backtracking
import re
re.match(r"(a+)+$", user_input) # exponential time on "aaaaaaaaaaaaaaaaX"
# ✅ Correct: use atomic patterns or limit input length, prefer re2
import re2 # google-re2: linear-time guarantees
def safe_match(pattern: str, value: str, max_len: int = 1000) -> bool:
if len(value) > max_len:
raise ValueError("Input too long")
return bool(re2.fullmatch(pattern, value))File Upload Validation
# ❌ Anti-pattern: trust file extension and user-supplied name
def save_upload(file):
file.save(f"/uploads/{file.filename}")
# ✅ Correct: validate type, size, and sanitize name
import uuid
import magic
from pathlib import PurePosixPath
ALLOWED_TYPES = {"image/png", "image/jpeg", "application/pdf"}
MAX_SIZE = 10 * 1024 * 1024 # 10 MB
def save_upload(file_content: bytes, original_name: str, upload_dir: Path) -> Path:
if len(file_content) > MAX_SIZE:
raise ValueError("File too large")
mime = magic.from_buffer(file_content[:2048], mime=True)
if mime not in ALLOWED_TYPES:
raise ValueError(f"Disallowed file type: {mime}")
ext = PurePosixPath(original_name).suffix.lower()
if ext not in {".png", ".jpg", ".jpeg", ".pdf"}:
raise ValueError("Invalid extension")
safe_name = f"{uuid.uuid4().hex}{ext}"
dest = upload_dir / safe_name
dest.write_bytes(file_content)
return destEmail, URL, and Path Validation
- Use
pydantic.EmailStr(backed byemail-validator) for emails. - Use
pydantic.HttpUrlorurllib.parse.urlparse+ scheme allowlist for URLs. - Validate paths with
pathlib.Path.resolve()and check against an allowed base directory.
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"https"}
def validate_url(url: str) -> str:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError("Only HTTPS URLs allowed")
if not parsed.hostname:
raise ValueError("Missing hostname")
return urlKey rules:
- Always use
re.fullmatch, neverre.matchfor validation (avoids partial matches). - Limit input length before regex evaluation.
- Prefer pydantic or marshmallow for structured input over manual checks.
- Validate MIME type by reading file magic bytes, never trust extension alone.
---
2. Authentication and Authorization Patterns
Hash passwords with modern, memory-hard algorithms. Enforce least-privilege at every layer.
Password Hashing
# ❌ Anti-pattern: plain hash
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
# ✅ Correct: argon2-cffi (preferred) or bcrypt
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hashed = ph.hash(password)
# Verification
try:
ph.verify(hashed, password_attempt)
except argon2.exceptions.VerifyMismatchError:
raise AuthenticationError("Invalid credentials")# Alternative: bcrypt
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
assert bcrypt.checkpw(password_attempt.encode(), hashed)JWT Best Practices
# ❌ Anti-pattern: no expiry, algorithm confusion
import jwt
token = jwt.encode({"user": "admin"}, key, algorithm="HS256")
data = jwt.decode(token, key, algorithms=["HS256", "none"]) # allows "none"!
# ✅ Correct: explicit algorithm, expiry, issuer, audience
import jwt
from datetime import datetime, timedelta, timezone
def create_token(user_id: str, secret: str) -> str:
return jwt.encode(
{
"sub": user_id,
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"iss": "myapp",
"aud": "myapp-api",
},
secret,
algorithm="HS256",
)
def verify_token(token: str, secret: str) -> dict:
return jwt.decode(
token, secret,
algorithms=["HS256"], # single allowed algorithm
options={"require": ["exp", "iss", "sub"]},
issuer="myapp",
audience="myapp-api",
)Decorator-Based Permission Checks
from functools import wraps
def require_role(*roles: str):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
user = get_current_user()
if user.role not in roles:
raise PermissionError("Insufficient permissions")
return fn(*args, **kwargs)
return wrapper
return decorator
@require_role("admin", "editor")
def delete_article(article_id: int): ...Session Management
- Set
HttpOnly,Secure,SameSite=Lax(orStrict) on session cookies. - Regenerate session ID after login to prevent session fixation.
- Enforce absolute session timeout (e.g., 24h) and idle timeout (e.g., 30min).
- Store sessions server-side; never store sensitive data in client-side cookies.
Key rules:
- Never store plaintext passwords. Use argon2-cffi as the default recommendation.
- Always pin JWT algorithm to a single value in
algorithms=[...]. - Always require
expclaim in JWTs. Keep token lifetime short (<1h for access tokens). - Use refresh tokens (opaque, stored server-side) for long-lived sessions.
- Apply RBAC checks at the route/endpoint level, not just in the UI.
---
3. Cryptography Best Practices
Use the cryptography library for all cryptographic operations. Never implement custom cryptographic algorithms.
Symmetric Encryption (Fernet)
# ❌ Anti-pattern: ECB mode, manual IV
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB) # ECB leaks patterns
# ✅ Correct: Fernet (AES-128-CBC + HMAC-SHA256, built-in IV + timestamp)
from cryptography.fernet import Fernet
key = Fernet.generate_key() # store securely
f = Fernet(key)
token = f.encrypt(plaintext.encode())
plaintext = f.decrypt(token).decode()Asymmetric Encryption (RSA)
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
public_key = private_key.public_key()
ciphertext = public_key.encrypt(
plaintext.encode(),
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
)Secure Randomness
# ❌ Anti-pattern: random module is NOT cryptographically secure
import random
token = "".join(random.choices("abcdef0123456789", k=32))
# ✅ Correct: secrets module
import secrets
token = secrets.token_urlsafe(32) # URL-safe base64
hex_token = secrets.token_hex(32) # hex stringHashing and Key Derivation
import hashlib
# Integrity hashing (non-password)
digest = hashlib.sha256(data).hexdigest()
digest = hashlib.blake2b(data, digest_size=32).hexdigest()
# Key derivation (password → key)
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import os
salt = os.urandom(16)
kdf = Scrypt(salt=salt, length=32, n=2**17, r=8, p=1)
key = kdf.derive(password.encode())Envelope Encryption
Encrypt data with a unique data encryption key (DEK), then encrypt the DEK with a key encryption key (KEK).
from cryptography.fernet import Fernet
def envelope_encrypt(plaintext: bytes, kek: bytes) -> tuple[bytes, bytes]:
dek = Fernet.generate_key()
encrypted_data = Fernet(dek).encrypt(plaintext)
encrypted_dek = Fernet(kek).encrypt(dek)
return encrypted_data, encrypted_dekKey rules:
- Never use
randomfor security-sensitive values. Always usesecrets. - Never use MD5 or SHA-1 for security purposes.
- Use RSA key size ≥ 3072 bits (prefer 4096).
- Always use OAEP padding for RSA encryption, PSS for RSA signatures.
- Validate TLS certificates; never set
verify=Falsein production. - Store keys in a secrets manager, never in source code.
---
4. Secure Data Handling
Minimize data exposure. Store secrets outside the codebase. Compare sensitive values in constant time.
Secrets Management
# ❌ Anti-pattern: hardcoded secrets
DB_PASSWORD = "supersecret123"
# ✅ Correct: environment variables with validation
import os
def get_required_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
DB_PASSWORD = get_required_env("DB_PASSWORD")# ✅ Vault integration (hvac)
import hvac
client = hvac.Client(url="https://vault.example.com", token=os.environ["VAULT_TOKEN"])
secret = client.secrets.kv.v2.read_secret_version(path="db/creds")
db_password = secret["data"]["data"]["password"]Secure String Comparison
# ❌ Anti-pattern: timing side-channel
if token == expected_token: # early exit leaks length info
...
# ✅ Correct: constant-time comparison
import hmac
if hmac.compare_digest(token.encode(), expected_token.encode()):
...Memory Handling
# Clear sensitive data after use (best-effort in CPython)
import ctypes
def secure_clear(s: bytearray):
"""Zero out a bytearray in place."""
ctypes.memset((ctypes.c_char * len(s)).from_buffer(s), 0, len(s))
password = bytearray(b"secret")
try:
process(password)
finally:
secure_clear(password)PII and Data Minimization
- Collect only necessary PII fields. Strip unnecessary data at the boundary.
- Hash or tokenize identifiers when full values are not needed.
- Apply field-level encryption for sensitive columns (SSN, credit card).
- Log redacted values only:
email=j***@example.com.
Key rules:
- Never commit secrets to version control. Use
.envfiles excluded via.gitignore, or vault. - Always use
hmac.compare_digestfor token/signature comparison. - Use
bytearray(mutable) instead ofstr/bytes(immutable) for secrets when clearing is needed. - Redact PII in all log output.
---
5. File and Path Operations
Canonicalize all paths before use. Never construct file paths from raw user input.
Path Traversal Prevention
# ❌ Anti-pattern: direct concatenation
def read_file(user_path: str) -> bytes:
return open(f"/data/{user_path}", "rb").read() # "../../../etc/passwd"
# ✅ Correct: resolve and check prefix
from pathlib import Path
BASE_DIR = Path("/data").resolve()
def safe_read(user_path: str) -> bytes:
target = (BASE_DIR / user_path).resolve()
if not target.is_relative_to(BASE_DIR):
raise ValueError("Path traversal detected")
return target.read_bytes()Secure Temporary Files
# ❌ Anti-pattern: predictable temp file
with open("/tmp/myapp_data.txt", "w") as f:
f.write(secret)
# ✅ Correct: tempfile module (unpredictable name, secure permissions)
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=True) as f:
f.write(secret)
f.flush()
process(f.name)
# File auto-deleted on closeSymlink Attack Prevention
import os
from pathlib import Path
def safe_open(path: Path, base_dir: Path):
resolved = path.resolve()
if not resolved.is_relative_to(base_dir.resolve()):
raise ValueError("Symlink escape detected")
if resolved.is_symlink():
raise ValueError("Symlinks not allowed")
return resolved.open("r")File Permission Management
import os, stat
# Set restrictive permissions: owner read/write only
os.chmod(filepath, stat.S_IRUSR | stat.S_IWUSR) # 0o600
# Create file with restricted permissions from the start
fd = os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
f.write(secret_data)Key rules:
- Always call
.resolve()on user-supplied paths and verify prefix with.is_relative_to(). - Use
tempfilemodule for all temporary files—never construct temp paths manually. - Set file permissions to
0o600for sensitive files. - Check
os.path.islink()orPath.is_symlink()before operating on files when symlink attacks are a concern.
---
6. Subprocess and System Interaction
Never pass untrusted input through a shell. Use list-form arguments to prevent injection.
Shell Injection Prevention
# ❌ Anti-pattern: shell=True with user input
import subprocess
subprocess.run(f"grep {user_input} /var/log/app.log", shell=True) # injection!
# ✅ Correct: list arguments, no shell
subprocess.run(["grep", "--", user_input, "/var/log/app.log"], check=True, capture_output=True)When Shell Is Unavoidable
# ✅ Use shlex.quote for shell escaping
import shlex
cmd = f"echo {shlex.quote(user_input)}"
subprocess.run(cmd, shell=True, check=True)Avoiding Dangerous Functions
# ❌ Never use these with untrusted input:
os.system(f"rm {filename}") # shell injection
os.popen(f"cat {filename}") # shell injection
eval(user_expression) # arbitrary code execution
exec(user_code) # arbitrary code execution
# ✅ Use subprocess.run with list form:
subprocess.run(["rm", "--", filename], check=True)Environment Variable Injection
# ❌ Anti-pattern: inherit full environment
subprocess.run(cmd, env=os.environ) # leaks secrets to child process
# ✅ Correct: explicit minimal environment
safe_env = {"PATH": "/usr/bin", "LANG": "C.UTF-8"}
subprocess.run(cmd, env=safe_env, check=True)Key rules:
- Default to
shell=False(the default). Use list-form[cmd, arg1, arg2]. - Never pass unsanitized user input to
os.system,os.popen,eval, orexec. - Use
shlex.quoteonly when shell mode is truly required. - Pass a minimal, explicit
envdict to child processes. - Always use
check=Trueto catch subprocess failures.
---
7. Serialization and Deserialization
Treat deserialization of untrusted data as code execution. Use safe-by-default formats.
Pickle: Never With Untrusted Data
# ❌ Anti-pattern: arbitrary code execution
import pickle
data = pickle.loads(untrusted_bytes) # can execute arbitrary code
# ✅ Correct: use JSON or other safe formats
import json
data = json.loads(untrusted_string) # safe: only produces dicts, lists, primitivesYAML: Always safe_load
# ❌ Anti-pattern: yaml.load allows arbitrary Python objects
import yaml
data = yaml.load(untrusted_string) # can instantiate arbitrary objects
# ✅ Correct: yaml.safe_load or yaml.SafeLoader
data = yaml.safe_load(untrusted_string)
# or
data = yaml.load(untrusted_string, Loader=yaml.SafeLoader)XML: Prevent XXE
# ❌ Anti-pattern: stdlib XML parsers allow external entities
from xml.etree.ElementTree import parse
tree = parse(untrusted_file) # XXE, billion laughs attack
# ✅ Correct: defusedxml
import defusedxml.ElementTree as ET
tree = ET.parse(untrusted_file) # blocks DTD, external entities, entity expansionSafer Alternatives
- JSON — safe by default, use
json.loads/json.dumps. - MessagePack —
msgpack.unpackb(data, raw=False)— compact binary, no code execution. - Protocol Buffers — schema-defined, typed, no arbitrary code execution.
Key rules:
- Never
pickle.loadsorpickle.loadon data from untrusted sources. - Never use
yaml.loadwithoutLoader=yaml.SafeLoader. Preferyaml.safe_load. - Replace
xml.etree.ElementTreewithdefusedxml.ElementTreefor untrusted XML. - Prefer JSON for data interchange; use protobuf or msgpack for performance-critical paths.
---
8. Web Framework Security
Apply framework-specific security defaults. Never disable protections without compensating controls.
Django
CSRF and Clickjacking
# settings.py — ensure these are NOT disabled
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", # CSRF protection
"django.middleware.clickjacking.XFrameOptionsMiddleware", # clickjacking
]
X_FRAME_OPTIONS = "DENY"
CSRF_COOKIE_HTTPONLY = TrueORM Safety
# ❌ Anti-pattern: raw SQL with string formatting
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{name}'")
# ✅ Correct: parameterized queries
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [name])
# or use the ORM
User.objects.filter(username=name)Template Safety
# ❌ Anti-pattern: marking untrusted content as safe
from django.utils.safestring import mark_safe
return mark_safe(user_input) # XSS
# ✅ Django auto-escapes by default in templates. Never mark_safe user input.
# Use |escape filter explicitly when needed.Security Settings Checklist
# settings.py (production)
SECRET_KEY = get_required_env("DJANGO_SECRET_KEY")
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = TrueFlask
CSRF Protection
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
app.config["SECRET_KEY"] = get_required_env("FLASK_SECRET_KEY")
csrf = CSRFProtect(app)Secure Session Configuration
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
PERMANENT_SESSION_LIFETIME=timedelta(hours=1),
)Jinja2 Auto-Escaping
# Jinja2 auto-escapes HTML in .html templates by default.
# ❌ Never use |safe with user input:
{{ user_input | safe }} {# XSS #}
# ✅ Let auto-escaping work:
{{ user_input }}FastAPI
Dependency Injection for Auth
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
payload = verify_token(token, SECRET_KEY)
user = await get_user(payload["sub"])
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
@app.get("/me")
async def read_me(user: User = Depends(get_current_user)):
return userCORS Configuration
from fastapi.middleware.cors import CORSMiddleware
# ❌ Anti-pattern:
app.add_middleware(CORSMiddleware, allow_origins=["*"]) # too permissive
# ✅ Correct: explicit origins
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_methods=["GET", "POST"],
allow_headers=["Authorization"],
allow_credentials=True,
)Rate Limiting
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.get("/api/search")
@limiter.limit("10/minute")
async def search(request: Request, q: str):
...Key rules (all frameworks):
- Never set
DEBUG = Truein production. - Always use parameterized queries or the ORM—never string-format SQL.
- Never mark user input as safe/trusted in templates.
- Configure CORS with explicit origin allowlists.
- Set all cookie security flags:
Secure,HttpOnly,SameSite. - Apply rate limiting on authentication and sensitive endpoints.
---
9. Error Handling and Information Disclosure
Expose minimal information to clients. Log full details server-side only.
Stack Trace Exposure
# ❌ Anti-pattern: leaking internals
@app.errorhandler(Exception)
def handle_error(e):
return {"error": str(e), "trace": traceback.format_exc()}, 500
# ✅ Correct: generic message to client, full details to log
import logging
logger = logging.getLogger(__name__)
@app.errorhandler(Exception)
def handle_error(e):
logger.exception("Unhandled exception") # full trace to server log
return {"error": "An internal error occurred"}, 500Custom Error Handlers
class AppError(Exception):
"""Base application error with safe external message."""
def __init__(self, message: str, status_code: int = 400, internal: str | None = None):
self.message = message # safe for client
self.internal = internal # logged only
self.status_code = status_code
@app.errorhandler(AppError)
def handle_app_error(e: AppError):
if e.internal:
logger.error(f"AppError: {e.internal}")
return {"error": e.message}, e.status_codeException Handling Patterns
# ❌ Anti-pattern: swallowing security exceptions
try:
verify_signature(token)
except Exception:
pass # silently continues with unverified token
# ✅ Correct: catch specific exceptions, never swallow auth/crypto errors
try:
verify_signature(token)
except InvalidSignatureError:
logger.warning("Invalid token signature", extra={"token_prefix": token[:8]})
raise HTTPException(status_code=401, detail="Invalid token")Key rules:
- Never return
traceback, exception messages, or SQL errors to the client. - Use a two-tier error model: safe message for the response, detailed message for the log.
- Never use bare
except: passfor security-sensitive operations. - Log at
WARNINGorERRORfor auth failures; include non-sensitive context. - Disable
DEBUGmode in production for all frameworks.
---
10. Concurrency and Race Conditions
Eliminate TOCTOU gaps. Use atomic operations and database-level locking for critical sections.
TOCTOU Vulnerabilities
# ❌ Anti-pattern: check-then-act race condition
import os
if os.path.exists(filepath): # T1: check
os.remove(filepath) # T2: use — file may have changed
# ✅ Correct: act and handle failure atomically
try:
os.remove(filepath)
except FileNotFoundError:
pass # already gone — expected in concurrent contextDatabase-Level Locking
# ❌ Anti-pattern: read-modify-write without locking
user = User.objects.get(id=user_id)
user.balance -= amount
user.save() # lost update if concurrent
# ✅ Correct: SELECT FOR UPDATE (Django ORM)
from django.db import transaction
with transaction.atomic():
user = User.objects.select_for_update().get(id=user_id)
if user.balance < amount:
raise InsufficientFundsError()
user.balance -= amount
user.save()# ✅ Alternative: F() expressions for atomic updates
from django.db.models import F
User.objects.filter(id=user_id).update(balance=F("balance") - amount)Atomic File Operations
# ❌ Anti-pattern: direct write (partial content on crash)
with open(target, "w") as f:
f.write(new_content)
# ✅ Correct: write-to-temp then atomic rename
import tempfile, os
def atomic_write(target: str, content: str):
dir_name = os.path.dirname(target)
with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as tmp:
tmp.write(content)
tmp.flush()
os.fsync(tmp.fileno())
os.replace(tmp.name, target) # atomic on POSIXThread Safety With Secrets
# ❌ Anti-pattern: shared mutable secret state
api_key = None # global mutable
def rotate_key():
global api_key
api_key = fetch_new_key() # race with readers
# ✅ Correct: threading.Lock or thread-local
import threading
_lock = threading.Lock()
_api_key: str = ""
def get_key() -> str:
with _lock:
return _api_key
def rotate_key():
new_key = fetch_new_key()
with _lock:
global _api_key
_api_key = new_keyKey rules:
- Never separate permission/existence checks from the operation—use EAFP (try/except).
- Use
SELECT FOR UPDATEorF()expressions for database write contention. - Use
os.replace()(atomic rename) for safe file updates. - Protect shared mutable state with
threading.Lock. - Prefer database constraints (unique, check) over application-level checks for invariants.
Python Security Verification Checklist
Purpose: Actionable security checklists for Python applications.
Use during code review, pre-deployment audits, and ongoing security assessments.
---
Table of Contents
- 1. Code-Level Security Checklist
- 1.1 Dangerous Functions and Patterns
- 1.2 Input Handling
- 1.3 Cryptography and Secrets
- 1.4 Error Handling and Logging
- 1.5 File and Resource Operations
- 1.6 Serialization and Parsing
- 2. Application Architecture Security Checklist
- 2.1 Authentication and Authorization
- 2.2 HTTP and API Security
- 2.3 Data and Storage Security
- 2.4 Application Design
- 3. Dependency Security Checklist
- 4. Configuration and Secrets Checklist
- 5. Deployment Security Checklist
- 6. Testing Security Checklist
- 7. Security Tools Reference
- 8. Incident Response Checklist
---
1. Code-Level Security Checklist
1.1 Dangerous Functions and Patterns
OWASP: A05:2025 Injection
- [ ] Verify no use of
eval(),exec(), orcompile()with untrusted input - [ ] Verify no use of
__import__()with user-controlled module names - [ ] Verify no
os.system()calls — usesubprocess.run()with list arguments instead - [ ] Verify no
shell=Trueinsubprocesscalls when command includes user input - [ ] Verify no
os.popen()usage — replace withsubprocess.run() - [ ] Verify no string formatting (
f"",.format(),%) in SQL queries — use parameterized queries exclusively - [ ] Verify no use of
string.Templatefor constructing SQL, shell commands, or HTML - [ ] Verify no
getattr()/setattr()with user-controlled attribute names - [ ] Verify no
globals()orlocals()manipulation based on user input - [ ] Verify
assertstatements are not used for security checks (stripped by-Oflag) - [ ] Verify no use of
input()in Python 2 (equivalent toeval(input())) - [ ] Verify no
marshal.loads()with untrusted data - [ ] Verify no use of
ctypeswith user-controlled arguments
1.2 Input Handling
OWASP: A05:2025 Injection
- [ ] Validate all input at every entry point (CLI args, env vars, API params, file reads)
- [ ] Enforce maximum input length on all string inputs
- [ ] Validate and sanitize file names received from users
- [ ] Normalize Unicode input before validation to prevent bypass via homoglyphs
- [ ] Validate numeric inputs for range, type, and overflow
- [ ] Reject unexpected fields in structured input (use strict schema validation)
- [ ] Validate email addresses with a proper library (not just regex)
- [ ] Validate URL inputs — restrict allowed schemes to
https://where possible - [ ] Decode and validate all percent-encoded input before processing
- [ ] Apply type hints and runtime validation (e.g., pydantic) on all public API boundaries
1.3 Cryptography and Secrets
OWASP: A04:2025 Cryptographic Failures
- [ ] Verify no use of
randommodule for security purposes — usesecretsmodule - [ ] Verify no MD5 or SHA1 for password hashing — use
bcrypt,argon2, orscrypt - [ ] Verify no hardcoded secrets, passwords, API keys, or tokens in source code
- [ ] Verify cryptographic keys are of sufficient length (RSA ≥ 2048, AES ≥ 256)
- [ ] Verify no use of deprecated ciphers (DES, RC4, Blowfish)
- [ ] Verify
hmac.compare_digest()is used for constant-time comparison of secrets - [ ] Verify TLS certificate verification is not disabled (
verify=Falseinrequests) - [ ] Verify tokens and session IDs are generated with
secrets.token_urlsafe()or equivalent - [ ] Verify password hashing uses per-user salts (automatic with bcrypt/argon2)
- [ ] Verify sensitive data is zeroed from memory after use where feasible
1.4 Error Handling and Logging
OWASP: A09:2025 Security Logging and Alerting Failures, A10:2025 Mishandling of Exceptional Conditions
- [ ] Verify no bare
except:clauses — catch specific exceptions - [ ] Verify no sensitive data (passwords, tokens, PII) in error messages
- [ ] Verify no sensitive data written to log files
- [ ] Verify stack traces are not exposed to end users in production
- [ ] Verify exceptions do not reveal internal paths, database schemas, or infrastructure details
- [ ] Verify logging uses structured format (JSON) with consistent fields
- [ ] Verify log levels are appropriate (no DEBUG in production)
- [ ] Verify user-supplied data in logs is sanitized to prevent log injection
- [ ] Verify
finallyblocks are used for resource cleanup - [ ] Verify no silent exception swallowing (
except: pass) - [ ] Verify fail-closed error handling — operations fail to a secure state (e.g., transaction rollback on error)
- [ ] Verify resource cleanup on exceptions uses context managers (
withstatements) rather than manual cleanup - [ ] Verify centralized error handling is implemented to ensure consistent, safe error responses
- [ ] Verify rate limiting on error-prone endpoints to prevent cascading failures from exceptional conditions
1.5 File and Resource Operations
OWASP: A01:2025 Broken Access Control
- [ ] Verify path traversal checks on all file operations with user-supplied paths
- [ ] Use
pathlib.Path.resolve()and validate against an allowed base directory - [ ] Verify file permissions are set restrictively on created files (
0o600or0o644) - [ ] Verify temporary files use
tempfile.mkstemp()ortempfile.TemporaryDirectory() - [ ] Verify file descriptors and connections are closed properly (use context managers)
- [ ] Verify no symlink-following on user-supplied paths without validation
- [ ] Verify file size is checked before reading to prevent memory exhaustion
- [ ] Verify uploaded files are stored outside the webroot
- [ ] Verify file type validation checks magic bytes, not just file extension
- [ ] Verify no world-writable files or directories are created
1.6 Serialization and Parsing
OWASP: A08:2025 Software or Data Integrity Failures
- [ ] Verify no
pickle.loads()/pickle.load()with untrusted data - [ ] Verify no
yaml.load()— useyaml.safe_load()exclusively - [ ] Verify XML parsing uses
defusedxml(notxml.etree.ElementTreewith untrusted data) - [ ] Verify no
shelvemodule used with untrusted data (uses pickle internally) - [ ] Verify JSON parsing enforces size limits on untrusted input
- [ ] Verify
jsonpickleis not used with untrusted input - [ ] Verify no
dillorcloudpickledeserialization of untrusted data - [ ] Verify protobuf/msgpack schemas are strictly defined
- [ ] Verify CSV parsing handles injection payloads (formulas starting with
=,+,-,@)
---
2. Application Architecture Security Checklist
2.1 Authentication and Authorization
OWASP: A01:2025 Broken Access Control, A07:2025 Authentication Failures
- [ ] Enforce authentication at middleware/decorator level, not inline in views
- [ ] Implement authorization checks at object level, not just endpoint level
- [ ] Configure rate limiting on authentication endpoints (login, password reset, OTP)
- [ ] Enforce account lockout or exponential backoff after repeated failed login attempts
- [ ] Implement multi-factor authentication for sensitive operations
- [ ] Set secure session configuration:
HttpOnly,Secure,SameSite=LaxorStrict - [ ] Enforce session expiry and idle timeout
- [ ] Invalidate sessions server-side on logout
- [ ] Regenerate session ID on privilege elevation (login, role change)
- [ ] Verify JWT tokens have expiration (
exp), issuer (iss), and audience (aud) claims - [ ] Verify JWT signature algorithm is explicitly specified (prevent
alg: noneattacks) - [ ] Verify password reset tokens are single-use and time-limited
2.2 HTTP and API Security
OWASP: A02:2025 Security Misconfiguration, A01:2025 Broken Access Control (SSRF)
- [ ] Restrict CORS to specific origins — no wildcard (
*) in production - [ ] Enable CSRF protection on all state-changing endpoints
- [ ] Configure Content Security Policy (CSP) headers
- [ ] Set
X-Content-Type-Options: nosniffheader - [ ] Set
X-Frame-Options: DENYorSAMEORIGINheader - [ ] Set
Referrer-Policy: strict-origin-when-cross-originor stricter - [ ] Set
Permissions-Policyheader to disable unused browser features - [ ] Enforce HTTPS-only with HSTS header (
Strict-Transport-Security) - [ ] Implement request size limits at the web server / framework level
- [ ] Validate
Content-Typeheader matches expected format on all endpoints - [ ] Implement API versioning strategy to manage breaking security changes
- [ ] Return consistent, safe error response format (no stack traces, no internal details)
- [ ] Implement request ID tracing for security event correlation
2.3 Data and Storage Security
OWASP: A05:2025 Injection, A04:2025 Cryptographic Failures
- [ ] Use parameterized queries or ORM exclusively — no raw SQL string concatenation
- [ ] Configure database connections with least-privilege accounts
- [ ] Encrypt sensitive data at rest (PII, financial data, health records)
- [ ] Encrypt database connections (TLS for PostgreSQL, MySQL, etc.)
- [ ] Implement data retention and deletion policies
- [ ] Validate and sanitize data before storing and before rendering
- [ ] Restrict file uploads by type, size, and store outside the webroot
- [ ] Configure Redis/Memcached with authentication and network restrictions
- [ ] Implement audit logging for data access and modifications
- [ ] Verify database migrations do not drop security-relevant columns/constraints
2.4 Application Design
OWASP: A06:2025 Insecure Design, A10:2025 Mishandling of Exceptional Conditions
- [ ] Implement centralized error handling with safe error responses
- [ ] Use structured logging without sensitive data
- [ ] Separate configuration by environment (dev, staging, production)
- [ ] Implement health check endpoints that do not expose sensitive information
- [ ] Design for fail-secure: deny access by default on errors
- [ ] Implement circuit breakers for external service calls
- [ ] Enforce timeouts on all external HTTP requests and database queries
- [ ] Use async task queues (Celery) for long-running operations — validate task payloads
- [ ] Implement graceful shutdown to avoid data corruption
- [ ] Apply the principle of least privilege across all service boundaries
---
3. Dependency Security Checklist
OWASP: A03:2025 Software Supply Chain Failures
- [ ] Pin all dependencies to exact versions in
requirements.txtorpyproject.toml - [ ] Run
pip-auditwith no known vulnerabilities reported - [ ] Run
safety checkand resolve all findings - [ ] Remove unused dependencies from requirements files
- [ ] Verify all dependencies are sourced from official PyPI (no unverified custom indexes)
- [ ] Use
--require-hashesinrequirements.txtfor integrity verification - [ ] Use a virtual environment — no global pip installs in production
- [ ] Configure Dependabot or Renovate for automated dependency update PRs
- [ ] Verify license compliance for all dependencies (direct and transitive)
- [ ] Review transitive dependencies for known vulnerabilities
- [ ] Verify no dependency uses
setup.pywith arbitrary code execution during install - [ ] Verify no typosquatting risk — double-check package names
- [ ] Lock file (
poetry.lock,Pipfile.lock) committed and up to date - [ ] Review dependency changelogs before major version upgrades
- [ ] Verify no dependency has been abandoned (check last release date, maintainer activity)
- [ ] Test the application after every dependency update before deploying
---
4. Configuration and Secrets Checklist
OWASP: A02:2025 Security Misconfiguration, A04:2025 Cryptographic Failures
- [ ] Verify no secrets exist in source code or version control history
- [ ] Add
.envfiles to.gitignore - [ ] Separate environment-specific configurations (dev, staging, production)
- [ ] Load
SECRET_KEYand all secret values from environment variables or a secrets vault - [ ] Disable
DEBUGmode and development features in production - [ ] Verify database credentials are not hardcoded
- [ ] Rotate API keys, tokens, and credentials on a regular schedule
- [ ] Configure pre-commit hooks:
detect-secrets,gitleaks, or equivalent - [ ] Verify
settings.py(Django) or equivalent config does not contain secrets - [ ] Validate that all required configuration is present at startup (fail fast)
- [ ] Set restrictive file permissions on configuration files (
0o600) - [ ] Use a secrets manager (HashiCorp Vault, AWS Secrets Manager, etc.) for production secrets
- [ ] Verify
.env.exampleexists with placeholder values (not real secrets) - [ ] Audit git history for accidentally committed secrets (
git log -p --all -S 'password') - [ ] Verify CI/CD secrets are scoped to the minimum required repositories and environments
- [ ] Use short-lived credentials and tokens where possible
---
5. Deployment Security Checklist
OWASP: A02:2025 Security Misconfiguration
- [ ] Run production processes as a non-root user with minimal privileges
- [ ] Enforce HTTPS/TLS on all endpoints — redirect HTTP to HTTPS
- [ ] Configure security headers: HSTS,
X-Content-Type-Options,X-Frame-Options, CSP - [ ] Scan container images for vulnerabilities (Trivy, Snyk, Grype)
- [ ] Verify health check endpoints do not expose sensitive information
- [ ] Configure logging to an external, tamper-resistant log store
- [ ] Verify error pages do not leak stack traces, internal paths, or version numbers
- [ ] Serve static files separately from the application (CDN or reverse proxy)
- [ ] Configure network-level access controls (firewall rules, security groups)
- [ ] Set up monitoring and alerting for security events (failed logins, privilege escalation)
- [ ] Use a minimal base container image (e.g.,
python:3.x-slimor distroless) - [ ] Remove development tools and debug packages from production images
- [ ] Verify no
.pycfiles or__pycache__directories are served publicly - [ ] Configure
gunicorn/uvicornwith appropriate worker count and timeouts - [ ] Enable access logging on the reverse proxy (nginx, Caddy, etc.)
- [ ] Implement automated rollback on failed deployment
- [ ] Verify DNS records do not expose internal hostnames
- [ ] Disable unnecessary network ports and services
- [ ] Implement infrastructure-as-code and review security of IaC templates
- [ ] Verify backup encryption and test restore procedures
---
6. Testing Security Checklist
- [ ] Write unit tests for all authentication paths (login, logout, token refresh)
- [ ] Write unit tests for all authorization paths (role-based, object-level)
- [ ] Write negative tests for access control — verify unauthorized access returns 403
- [ ] Test input validation with malicious payloads (overlong strings, null bytes, Unicode)
- [ ] Test all query endpoints against SQL injection payloads
- [ ] Test all output rendering against XSS payloads
- [ ] Test CSRF token validation — verify requests without tokens are rejected
- [ ] Test rate limiting under load — verify limits are enforced
- [ ] Test session management: fixation, expiry, invalidation, concurrent sessions
- [ ] Run
banditstatic analysis with no high-severity issues (bandit -r src/ -ll) - [ ] Run
safetyorpip-auditin CI pipeline — fail the build on findings - [ ] Test file upload validation with oversized files, wrong MIME types, and path traversal names
- [ ] Test API endpoints with unexpected HTTP methods (PUT, DELETE on read-only endpoints)
- [ ] Test for SSRF by supplying internal URLs as input
- [ ] Test for open redirect vulnerabilities on redirect endpoints
- [ ] Test password policy enforcement (minimum length, complexity)
- [ ] Test account lockout behavior after failed login attempts
- [ ] Test for information disclosure in error responses (404, 500, validation errors)
- [ ] Include security test fixtures in
conftest.pyfor authenticated/unauthenticated clients - [ ] Run
semgrepwith Python security rules in CI pipeline - [ ] Test for HTTP parameter pollution on all endpoints
- [ ] Verify test coverage on security-critical code paths is ≥ 90%
---
7. Security Tools Reference
| Tool | Purpose | Usage |
|---|---|---|
| bandit | Static analysis for common Python security issues | bandit -r src/ -ll -ii |
| safety | Check installed dependencies for known vulnerabilities | safety check --full-report |
| pip-audit | Audit pip dependencies against vulnerability databases | pip-audit --strict |
| detect-secrets | Pre-commit hook to prevent secrets from being committed | detect-secrets scan --all-files |
| semgrep | Advanced static analysis with custom and community rules | semgrep --config=p/python |
| pylint | Linting with security-related plugins | pylint --load-plugins=pylint_security src/ |
| mypy | Type checking to catch type-confusion vulnerabilities | mypy --strict src/ |
| trivy | Container image and filesystem vulnerability scanning | trivy image myapp:latest |
| grype | Container image vulnerability scanner | grype myapp:latest |
| gitleaks | Detect secrets in git history | gitleaks detect --source . |
| OWASP ZAP | Dynamic application security testing (DAST) | zap-cli quick-scan https://app |
| tox | Run security checks across multiple Python versions | tox -e security |
| coverage | Measure test coverage on security-critical paths | coverage run -m pytest tests/security/ |
| pre-commit | Framework for managing pre-commit hooks | pre-commit run --all-files |
| ossf-scorecard | Evaluate open-source project security posture | scorecard --repo=github.com/org/repo |
Recommended Pre-commit Configuration
# .pre-commit-config.yaml (security-related hooks)
repos:
- repo: https://github.com/PyCQA/bandit
rev: 1.8.3
hooks:
- id: bandit
args: ["-r", "src/", "-ll"]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
- repo: https://github.com/gitleaks/gitleaks
rev: v8.22.1
hooks:
- id: gitleaksRecommended CI Security Stage
# GitHub Actions example
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install bandit safety pip-audit semgrep
- run: bandit -r src/ -ll -ii
- run: pip-audit --strict
- run: semgrep --config=p/python --error---
8. Incident Response Checklist
Immediate Response (0–1 hour)
- [ ] Assess severity using CVSS or internal severity matrix
- [ ] Determine scope: which systems, data, and users are affected
- [ ] Assign an incident owner and notify the security team
- [ ] Contain the vulnerability — disable affected endpoints, revoke compromised tokens
- [ ] Preserve evidence — snapshot logs, affected systems, and artifacts before changes
Short-Term Remediation (1–24 hours)
- [ ] Rotate all potentially compromised credentials (API keys, database passwords, tokens)
- [ ] Patch the vulnerability in a dedicated branch
- [ ] Review code changes with a security-focused reviewer
- [ ] Deploy the fix to production through the standard pipeline (no hotfix shortcuts)
- [ ] Verify the fix resolves the vulnerability without introducing regressions
- [ ] Review logs for signs of exploitation (unusual access patterns, data exfiltration)
Communication and Compliance (24–72 hours)
- [ ] Notify affected users if personal data was exposed (per GDPR, CCPA, or applicable regulation)
- [ ] File required breach notifications with regulatory bodies if applicable
- [ ] Update internal security advisories and status pages
- [ ] Communicate timeline and remediation steps to stakeholders
Post-Incident (1–2 weeks)
- [ ] Conduct a blameless post-mortem with all involved parties
- [ ] Document root cause, timeline, impact, and remediation
- [ ] Identify process gaps that allowed the vulnerability
- [ ] Update security checklists, tests, and automation to prevent recurrence
- [ ] Add regression tests for the specific vulnerability
- [ ] Review related code for similar patterns
- [ ] Schedule follow-up review to verify all actions are completed
---
Quick Reference: Common Vulnerability Patterns
| Pattern | Vulnerable Code | Secure Alternative |
|---|---|---|
| Code injection | eval(user_input) | Parse and validate explicitly |
| SQL injection | f"SELECT * FROM t WHERE id={uid}" | cursor.execute("SELECT * FROM t WHERE id=%s", (uid,)) |
| Deserialization | pickle.loads(data) | json.loads(data) with schema validation |
| YAML bomb | yaml.load(data) | yaml.safe_load(data) |
| Command injection | os.system(f"ls {path}") | subprocess.run(["ls", path]) |
| Path traversal | open(f"uploads/{filename}") | Path(base / filename).resolve() + verify prefix |
| Weak randomness | random.randint(0, 999999) | secrets.randbelow(1000000) |
| Weak hashing | hashlib.md5(password) | bcrypt.hashpw(password, bcrypt.gensalt()) |
| SSRF | requests.get(user_url) | Validate URL against allowlist |
| Timing attack | token == user_token | hmac.compare_digest(token, user_token) |