Django advanced audit
TL;DR: Beyond the basics in django-audit-patterns: advanced Django audit covers SSRF in URLValidator /
requestspatterns, RCE viaeval/exec/os.systemchains, classic deserialisation in pickled sessions/cache, Django Admin sub-path bypasses, Q objects /extra()/raw()SQL injection, GIS / file-storage misconfig, channels websocket auth gaps, and Django REST Framework permission-class fallthrough. Companion to django-audit-patterns and python-deserialization.
Why advanced
Most Django apps audit cleanly for the standard OWASP top 10 because of safe defaults. The advanced class lives in:
- Patterns developers write to “escape” the safe defaults.
- DRF (Django REST Framework) misconfiguration.
- Custom serialiser / form / view code.
Class 1 — SSRF via fetched URLs
1
2
3
4
5
6
import requests
def fetch(request):
url = request.GET['url']
response = requests.get(url)
return HttpResponse(response.content)
Standard SSRF. See ssrf and ssrf-to-cloud-advanced-chains.
URLValidator doesn’t help — it validates URL syntax, not target IP.
Class 2 — eval / exec / subprocess
Less common in Django than in scripted Python projects, but appears in admin tools, data-pipeline code:
1
2
result = eval(request.POST['expr'])
subprocess.call(f"convert {filename}", shell=True)
Same as command-injection and Python eval anti-pattern.
Class 3 — Pickle deserialisation
Django used to support pickle session serializer (now JSON by default). Some codebases:
- Use
signed_cookieswith pickle for “convenience”. - Use cache backend that pickles values; if cache key is attacker-influenced, payload-controlled cache write → pickle-deserialise on read.
Pickle = RCE.
Class 4 — Q objects / extra() / raw() SQL
.raw(string) and .extra(where=string) accept raw SQL — interpolation = SQLi:
1
2
User.objects.extra(where=[f"role = '{role}'"]) # injection!
User.objects.raw(f"SELECT * FROM auth_user WHERE role = '{role}'")
Q objects are safer but constructed via **kwargs with user-controlled keys reach attribute lookup paths that may surprise.
Class 5 — Template engine SSTI
Django templates are safer than Jinja2 by default (limited expression set). But:
- Custom template tags can execute arbitrary code.
- Jinja2 used alongside Django (via
django.template.backends.jinja2) → SSTI.
If user input becomes a template string:
1
2
3
from django.template import Template, Context
t = Template(user_input)
t.render(Context({...}))
— rare but devastating.
Class 6 — Admin interface exposure
Django Admin at /admin/:
- Default URL.
- Often exposed publicly.
- If staff users have weak passwords or social engineering target, full database access.
Audit: is admin behind VPN / IP allowlist?
Class 7 — Admin model-with-property privilege escalation
Custom admin actions or callable fields can be called by any staff user. If a callable mutates state:
1
2
3
4
5
6
class UserAdmin(admin.ModelAdmin):
list_display = ['name', 'become_admin']
def become_admin(self, obj):
obj.is_superuser = True
obj.save()
A staff user clicking the “become_admin” column triggers escalation.
Class 8 — DRF permission_classes fallthrough
1
2
3
4
5
6
7
8
9
class MyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# ...
class AdminView(MyView):
# forgot permission_classes; inherits IsAuthenticated only
pass
Subclasses don’t always inherit tightening; check carefully.
Common DRF mistake: DEFAULT_PERMISSION_CLASSES = [] empty default + per-view explicit. One view missing the explicit = open.
Class 9 — DRF serialiser create / update bypass
1
2
3
4
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
fields = '__all__' includes is_superuser. POST with is_superuser=true = admin.
Audit: every Meta for fields = '__all__' or missing exclusions.
Class 10 — File storage / MEDIA_URL traversal
Custom file storage code that derives paths from user input:
1
2
def upload_path(instance, filename):
return f"uploads/{instance.user.username}/{filename}"
If filename is attacker-controlled and contains .., path-traversal.
FileField and ImageField sanitise by default; custom storage may not.
Class 11 — Django Channels (WebSocket) auth
channels adds WebSocket support. Authentication middleware order matters. Common mistake:
- HTTP routes require authentication.
- WebSocket consumer accepts without auth, processes messages.
Audit routing.py and consumer initialisation.
Class 12 — Signed-cookie / token reuse
Django’s signing framework signs but doesn’t encrypt. Tokens visible to user. If sensitive data leaked in token, information disclosure.
Tokens also don’t rotate; revocation requires re-key.
Class 13 — Static-files served by Django in production
runserver serves static; DEBUG=True enables. Production with DEBUG=True:
- Stack traces with environment leak.
- SQL queries shown.
static()URL pattern handles arbitrary path.
Most catastrophic mistake. Audit: DEBUG=False in production unequivocally.
Audit shape
For a Django app:
- Settings:
DEBUG,ALLOWED_HOSTS,SESSION_SERIALIZER,CACHES. - URLs: list all routes; identify auth class per.
- Views: grep for
eval,exec,subprocess,requests,Template(. - ORM: grep for
.extra(,.raw(, string interpolation in.where(. - Serializers: grep
fields = '__all__'; explicitexcludefor sensitive. - Admin: actions, callable methods.
- Channels: consumer auth.
- Middleware: order, custom logic.
- Django version against CVE history.
Defensive baseline
- DEBUG=False in production.
- JSON session serializer (Django default; verify).
- Explicit serializer fields — no
__all__. - Whitelist URL patterns — no catch-all when avoidable.
- DRF default permissions explicit and strict.
- Admin behind VPN / strong MFA.
- Static / media served by web server, not Django, in production.
bandit+semgrepin CI.safety/pip-auditfor dependency CVEs.
Workflow to study
- Use
django-DefectDojoor Django docs vulnerable-app examples. - Run
banditon a real Django project. - Read Django security advisories.
- Audit a real open-source Django project (Wagtail, Mezzanine).
Related
- django-audit-patterns
- python-code-auditing
- python-dangerous-sinks
- python-deserialization
- mass-assignment
- broken-access-control
- ssrf
- ssti
- rails-advanced-audit