Python Backend Development10 min read · May 2026Updated Jun 2026

SaaS Backend Security: The OWASP Top 10 Checklist for Python Developers

Security vulnerabilities are not exotic edge cases — they are predictable, recurring patterns that appear in SaaS applications when developers prioritize features over security basics. The OWASP Top 10 has remained largely consistent for a decade because the same categories of mistakes recur across languages and frameworks. This guide maps the OWASP Top 10 to Python backend development specifically, with concrete implementation patterns for FastAPI and Django.

Authentication and Session Management (OWASP A07)

Authentication bugs are the most common and most catastrophic vulnerability category. These are the specific patterns that appear repeatedly in Python SaaS codebases:

  • JWT expiry: set short expiry (15–60 minutes) for access tokens; use refresh tokens for long-lived sessions. Never set JWT expiry to "never".
  • Algorithm confusion: explicitly specify the JWT algorithm (HS256 or RS256). Libraries that accept "alg: none" are vulnerable to algorithm confusion attacks.
  • Password hashing: use bcrypt, scrypt, or Argon2 via passlib — never SHA256 or MD5, never store plaintext passwords
  • Brute-force protection: rate-limit login attempts per IP and per account (5 failures → 5-minute lockout)
  • Secure token storage: store JWTs in httpOnly cookies, not localStorage — XSS attacks cannot read httpOnly cookies
  • Implement proper logout: invalidate refresh tokens at logout (requires a token blocklist or short-lived tokens)

SQL Injection and Query Parameterization (OWASP A03)

SQL injection remains a top-10 vulnerability in 2026 despite being a solved problem. In Python, the solution is using parameterized queries consistently:

  • Never use f-strings or .format() to build SQL queries: cursor.execute(f"SELECT * FROM users WHERE id={user_id}") is vulnerable
  • Always use parameterized queries: cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) is safe
  • SQLAlchemy ORM: all ORM queries are parameterized by default — raw text queries using text() require explicit bind parameters
  • Django ORM: Django's QuerySet API is safe by default — raw SQL with User.objects.raw() requires careful parameterization
  • Input validation: validate and type-check all user inputs using Pydantic (FastAPI) or Django forms — reject unexpected data types
  • Least privilege: your database user should only have SELECT, INSERT, UPDATE, DELETE — never CREATE, DROP, or ALTER in production
Pydantic (used by FastAPI) provides automatic input validation that catches type mismatches before they reach your SQL queries. Using Pydantic models for all request bodies is both a correctness and security win.

Secrets Management and Environment Variables (OWASP A02)

Exposed secrets are the easiest attack vector. API keys, database passwords, and JWT secrets in source code are found by automated scanners within hours of repository publication:

  • Never commit secrets to source control — use environment variables, .env files (gitignored), or cloud secrets services
  • AWS Secrets Manager or GCP Secret Manager for production: inject secrets at container startup, rotate them automatically
  • Secret scanning in CI: GitHub Secret Scanning and tools like truffleHog scan commits for accidentally committed secrets
  • Rotate secrets after any exposure — if a secret appears in a commit, treat it as compromised immediately
  • Different secrets per environment: development, staging, and production must use separate API keys and database passwords
  • Audit your dependencies: third-party packages can leak secrets through logging — review dependency changelogs before upgrading

API Rate Limiting and Abuse Prevention (OWASP A04)

APIs without rate limiting are open to brute-force attacks, credential stuffing, scraping, and resource exhaustion. These are the layers of protection a production API should have:

  • Per-IP rate limiting: slowapi (FastAPI) or Django Ratelimit apply request limits per IP address
  • Per-user rate limiting: authenticated endpoints should limit requests per user token to prevent account abuse
  • Tiered limits: free tier users get 100 req/min; paid users get 1,000 req/min — enforce limits based on subscription tier
  • Slow-down middleware: instead of hard rejecting at the limit, progressively slow responses — makes brute-force attacks impractical
  • CAPTCHA for high-value endpoints: login, signup, and password reset endpoints should require CAPTCHA after N failures
  • Distributed rate limiting: use Redis as the backing store for rate limit counters — in-memory counters do not work behind a load balancer

Security Headers, CORS, and HTTPS

A significant portion of web security is enforced at the HTTP response header level. These are the headers that should be present on every production Python API response:

  • Strict-Transport-Security (HSTS): force HTTPS for all future requests — max-age=31536000; includeSubDomains
  • Content-Security-Policy: restrict what resources the browser can load — prevents XSS even if an attacker injects scripts
  • X-Content-Type-Options: nosniff — prevents MIME-sniffing attacks
  • X-Frame-Options: DENY — prevents clickjacking via iframes
  • CORS configuration: explicitly whitelist allowed origins — never use Access-Control-Allow-Origin: * for authenticated APIs
  • Referrer-Policy: strict-origin-when-cross-origin — controls how much URL information is sent in referrer headers

Implementation Checklist

  • Audit all SQL query construction for f-string interpolation — replace with parameterized queries
  • Verify JWT expiry settings: access tokens should expire in 15–60 minutes
  • Run git secrets or truffleHog on your repository to find any committed credentials
  • Implement per-IP and per-user rate limiting on all public endpoints
  • Configure all required security headers using a middleware library (secure.py for Python)
  • Audit CORS settings: never use wildcard origin (*) on authenticated APIs
  • Enable HTTPS everywhere and configure HSTS with a 1-year max-age
  • Run OWASP ZAP or Burp Suite against your staging environment before each major release
  • Set up dependency vulnerability scanning in CI (pip-audit for Python packages)
  • Implement structured logging for security events: login failures, permission denials, rate limit hits

Common Mistakes to Avoid

  • Trusting user input before validation — all user-supplied data is potentially malicious until proven otherwise.
  • JWTs without expiry or with year-long expiry — compromised tokens remain valid indefinitely.
  • Overly permissive CORS — allowing any origin on an authenticated API enables CSRF from any website.
  • Logging sensitive data: passwords, tokens, credit card numbers, and PII should never appear in log files.
  • Skipping security testing before major releases — security is a process, not a one-time audit.
  • Not monitoring for anomalous API usage patterns — repeated authentication failures and unusual access patterns are early indicators of an attack.

Frequently Asked Questions

How do I prevent SQL injection in Python?+
Always use parameterized queries. In raw psycopg2: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,)). In SQLAlchemy ORM: use ORM methods or explicitly bind parameters with bindparam() for raw SQL. In Django: use the QuerySet API (User.objects.filter(id=user_id)) for all queries. Never use Python string formatting (f-strings, .format(), %) to construct SQL queries — this is vulnerable regardless of input validation.
What is the most important security fix for a Python API?+
The single most impactful improvement for most SaaS APIs is proper secrets management: move all secrets (database passwords, API keys, JWT secrets) from source code and environment files to a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault). Exposed secrets cause more breaches than any other single vulnerability category — and the fix is straightforward.
Does FastAPI handle security automatically?+
FastAPI provides security utilities (OAuth2 flows, HTTP Bearer, API key authentication) and automatic input validation via Pydantic. These significantly reduce the surface area for authentication bugs and injection attacks. However, FastAPI does not automatically add security headers, configure rate limiting, manage secrets, or audit your SQL queries. Pydantic validation is not a substitute for parameterized queries. Think of FastAPI as removing significant security friction, not as providing security by default.
Should I run a penetration test on my SaaS product?+
Yes, but timing matters. Running a pentest on an MVP with basic security hardening in place is premature — fix the OWASP Top 10 first. Run your first professional penetration test when: you are processing sensitive data (payment information, health data, credentials), you are pursuing enterprise customers who require a security questionnaire, or you are approaching SOC 2 compliance. Before that milestone, use automated tools (OWASP ZAP, Bandit for Python static analysis, pip-audit for dependencies) to catch common vulnerabilities cheaply.
How do I implement rate limiting in FastAPI?+
Use slowapi, a FastAPI-compatible rate limiting library. Install slowapi, create a Limiter instance backed by Redis, and apply the @limiter.limit('100/minute') decorator to your endpoints. For per-user rate limiting, provide a key function that returns the authenticated user's ID from the request. Store rate limit state in Redis rather than in-memory — in-process state does not work correctly behind multiple worker processes or a load balancer.
Work with us

Need help applying these principles to your project? We build exactly this for startups worldwide.

Build Your Secure Backend
Related guides
FastAPI vs Django: Which Is Better for Startups?
9 min read
Designing Scalable Backend Architectures With Python
10 min read
Cloud Infrastructure Best Practices for Growing SaaS Products
9 min read