1. Core Security Foundations

  • Treat all input as untrusted. Validate strictly (whitelists over blacklists), normalize before checks, and enforce types and sizes.
  • Use framework security features instead of writing your own.
  • Least privilege: minimize DB, filesystem, and network permissions.
  • Secrets management: use environment variables or secret stores, never hardcode.
  • Dependency hygiene: pin and audit dependencies with pip-audit or Safety.
  • Secure HTTP headers: add HSTS, X-Frame-Options, CSP, and others.
  • Logging & monitoring: log relevant events, but never credentials.
  • Testing: integrate Bandit and Semgrep in CI.

2. Preventing SQL Injection (SQLi)

Principle: Never build queries using string concatenation.

✅ Safe Examples

psycopg2:

cur.execute("SELECT id, email FROM users WHERE id = %s", (user_id,))

sqlite3:

cur.execute("SELECT * FROM products WHERE category = ?", (category,))

SQLAlchemy ORM:

user = session.query(User).filter(User.email == email).one_or_none()

❌ Avoid

  • f-strings, % formatting, or .format() in SQL statements.
  • Unvalidated dynamic identifiers (table names, order-by fields).

Use a whitelist map for dynamic values:

ORDER_BY_MAP = {"name": User.name, "created": User.created_at}
order_col = ORDER_BY_MAP.get(request.args.get("order_by"), User.created_at)
q = session.query(User).order_by(order_col)

Extra Hardening

  • Use read-only or least privilege DB users.
  • Disable multiple statements per execute.
  • Apply database constraints (CHECK, NOT NULL, etc.).

3. Preventing Cross-Site Scripting (XSS)

Principle: Encode on output, not input.

Template Autoescaping

  • Django and Jinja2 autoescape by default. Keep it that way.
  • Avoid mark_safe unless sanitized with a library like bleach.

Example:

import bleach
clean_html = bleach.clean(user_html, tags=['b','i','a','ul','li'], attributes={'a': ['href','title']})

Content Security Policy (CSP)

Apply a strict CSP to block inline scripts:

default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:;
connect-src 'self'; base-uri 'self'; frame-ancestors 'none';

Use Flask-Talisman or similar middleware to set headers automatically.

Encoding in Scripts

<script>
  const data = {{ payload | tojson }};
</script>

4. Preventing Server-Side Request Forgery (SSRF)

Principle: Don’t allow user-controlled URLs to trigger server requests.

Core Defenses

  1. Allowlist destinations where possible.
  2. Enforce https only, reject file://, gopher://, data:, etc.
  3. Validate host resolution:
    • Reject internal/private IPs (127.0.0.0/8, 10.0.0.0/8, etc.).
    • Block cloud metadata endpoints (169.254.169.254).
    • Prevent redirects to unvalidated hosts.

Example Safe Fetch

from urllib.parse import urlparse
import ipaddress, socket, requests

BLOCKED_NETS = [
    ipaddress.ip_network("127.0.0.0/8"),
    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("169.254.0.0/16"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
    ipaddress.ip_network("fe80::/10"),
]

def is_safe_remote(url: str, allow_hosts=None):
    u = urlparse(url)
    if u.scheme != "https":
        return False, "Only https allowed"
    host = u.hostname
    if not host:
        return False, "Missing host"
    if allow_hosts and host not in allow_hosts:
        return False, "Not in allowlist"
    infos = socket.getaddrinfo(host, None)
    for fam, *_ in infos:
        ip = ipaddress.ip_address(infos[0][4][0])
        if any(ip in net for net in BLOCKED_NETS):
            return False, f"Blocked IP: {ip}"
    return True, host

def safe_fetch(url: str):
    ok, msg = is_safe_remote(url)
    if not ok:
        raise ValueError(msg)
    return requests.get(url, allow_redirects=False, timeout=5, stream=True)

Additional SSRF Tips

  • Never expose a raw “fetch this URL” API.
  • Limit response size and validate MIME type.
  • Block IMDS endpoints at network layer (IMDSv2 for AWS).

5. Python-Specific Secure Patterns

  • Deserialization: use yaml.safe_load; never unpickle untrusted data.
  • Command execution: use subprocess.run([...], shell=False).
  • File uploads: generate safe filenames, store outside web root, restrict types.
  • Authentication: use secure cookies with HttpOnly, Secure, and SameSite=Strict.
  • Crypto: use cryptography or passlib (bcrypt/Argon2).

6. Final Security Checklist

  • No unparameterized SQL.
  • Autoescape enabled for templates.
  • SSRF-safe outbound requests.
  • CSRF protection on all state-changing requests.
  • Strong CSP and headers.
  • Dependencies pinned and scanned.
  • Secrets never logged or hardcoded.
  • Least privilege enforced across DB, filesystem, and network.

In summary:
Combine strict input validation, contextual output encoding, and robust network egress control with a least-privilege architecture. Use the framework’s built-in defenses, modern security headers, and scanning tools in CI to keep Python applications resilient against SSRF, SQLi, and XSS.