Security is not optional. These practices protect your applications and users from common vulnerabilities.

Never Hardcode Secrets

  # BAD
API_KEY = "sk-abc123secret"

# GOOD — load from environment
import os
API_KEY = os.environ["API_KEY"]
  

Use .env files locally (never commit them) and secret managers in production (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault).

  from dotenv import load_dotenv
load_dotenv()
database_url = os.environ["DATABASE_URL"]
  

Input Validation

Never trust user input. Validate and sanitize everything:

  from pydantic import BaseModel, EmailStr, Field

class CreateUser(BaseModel):
    username: str = Field(min_length=3, max_length=50, pattern=r"^[a-zA-Z0-9_]+$")
    email: EmailStr
    age: int = Field(ge=0, le=150)

# Raises ValidationError on bad input
user = CreateUser(username="alice", email="[email protected]", age=30)
  

Prevent SQL Injection

Always use parameterized queries:

  import sqlite3

# BAD — vulnerable to SQL injection
username = request.form["username"]
cursor.execute(f"SELECT * FROM users WHERE name = '{username}'")

# GOOD — parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (username,))
  

With ORMs (Django ORM, SQLAlchemy), use their query APIs instead of raw SQL.

Avoid Command Injection

  import subprocess

# BAD
subprocess.run(f"convert {user_filename} output.png", shell=True)

# GOOD
subprocess.run(["convert", user_filename, "output.png"], check=True)
  

Never pass unsanitized user input to shell=True.

Safe Deserialization

  import json

# SAFE
data = json.loads(user_input)

# DANGEROUS — never deserialize untrusted pickle/yaml
import pickle
pickle.loads(user_input)  # Can execute arbitrary code!
  

Use json for data exchange. If you must use YAML, use yaml.safe_load().

Dependency Security

Scan dependencies for known vulnerabilities:

  pip install pip-audit
pip-audit

# Or with safety
pip install safety
safety check
  

Pin dependencies and update regularly. Automate scanning in CI.

Authentication & Authorization

  • Hash passwords with bcrypt or argon2 — never store plaintext
  • Use HTTPS everywhere in production
  • Implement rate limiting on login and API endpoints
  • Apply least privilege — users should only access what they need
  from passlib.hash import bcrypt

hashed = bcrypt.hash("user_password")
bcrypt.verify("user_password", hashed)  # True
  

Security Headers (Web Apps)

  # Flask example
@app.after_request
def set_security_headers(response):
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["Strict-Transport-Security"] = "max-age=31536000"
    return response
  

Security Checklist

  • No secrets in source code or git history
  • All user input validated
  • Parameterized database queries
  • Dependencies scanned for CVEs
  • HTTPS enforced in production
  • Authentication and authorization on all endpoints
  • Error messages don’t leak internal details
  • Logging excludes sensitive data (passwords, tokens)

Security is a continuous practice, not a one-time task.

OWASP Top 10 — Python Developer View

Risk Python Context Mitigation
Broken Access Control Missing permission checks on views Decorators, RBAC, test authorization
Cryptographic Failures Weak hashing, hardcoded keys bcrypt/argon2, env vars, TLS
Injection SQL, shell, template injection Parameterized queries, no shell=True
Insecure Design No threat modeling Security requirements in design phase
Security Misconfiguration DEBUG=True in production Environment-based settings
Vulnerable Components Outdated dependencies pip-audit, Dependabot, pin versions
Auth Failures Weak session handling Secure cookies, JWT expiry, MFA
Data Integrity Failures Unsigned packages Verify hashes, use lock files
Logging Failures No audit trail Log auth events, monitor anomalies
SSRF Unvalidated URL fetching Allowlist domains, block internal IPs

Web-Specific Threats

Cross-Site Scripting (XSS)

Escape user content in HTML templates:

  # Django — auto-escapes by default in templates
# {{ user_input }}  is safe

# Jinja2 / manual HTML
from markupsafe import escape
safe_html = f"<p>{escape(user_comment)}</p>"
  

Never use |safe or Markup() on untrusted input.

Cross-Site Request Forgery (CSRF)

Ensure state-changing requests require a CSRF token:

  # Django — enabled by default for POST forms
# {% csrf_token %} in templates

# Flask
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
  

CORS Misconfiguration

Restrict API origins in production:

  # FastAPI
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourapp.com"],  # not "*"
    allow_credentials=True,
    allow_methods=["GET", "POST"],
)
  

JWT Security

  # Set short expiry
token = jwt.encode(
    {"sub": user_id, "exp": datetime.utcnow() + timedelta(hours=1)},
    SECRET_KEY,
    algorithm="HS256",
)

# Validate on every request — never trust client-side claims alone
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
  
  • Use strong SECRET_KEY from environment
  • Prefer short-lived access tokens + refresh tokens
  • Never store JWTs in localStorage if XSS is a concern — use httpOnly cookies

Logging Without Leaking Secrets

  # BAD
logger.info(f"Connecting with password={password}")

# GOOD
logger.info("Database connection established")
logger.debug("Auth attempt for user_id=%s", user_id)  # no passwords/tokens
  

Filter sensitive fields before logging request bodies in APIs.