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
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
Need help applying these principles to your project? We build exactly this for startups worldwide.