Django audit patterns
TL;DR: Django’s defaults eliminate most classic bugs (ORM is parameterised, CSRF baked in, templates auto-escape). Audits target:
Raw/extraORM use,SafeString/mark_safereaching templates, missing object-level permission in DRF,picklein sessions/cache,eval/execin admin,ALLOWED_HOSTS=*, and Debug Toolbar in prod.
Common bug patterns
1. ORM raw queries
User.objects.raw("SELECT * FROM users WHERE name='%s'" % name)→ SQLi..extra(where=["name='" + name + "'"])→ SQLi.connection.cursor()thencursor.execute(f"... {x}")→ SQLi.- Safe:
.filter(name=x),.raw("SELECT * FROM users WHERE name=%s", [name])(parameterised).
2. Template injection / mark_safe
- Django templates auto-escape, but
{{ x|safe }},mark_safe(x),format_html("{}", x)(without sub-escaping) bypass. render_to_string(template_name)withtemplate_namefrom user input = LFI/RCE if app has writable template dirs.- Jinja2 backend if configured (
django.template.backends.jinja2) — different sandbox; see python-ssti-jinja.
3. DRF object-level permissions
DEFAULT_PERMISSION_CLASSEScovers presence (auth, role), not object ownership.- ViewSets need explicit
get_queryset(self)filter torequest.userORhas_object_permissiononBasePermission. Without either, any authenticated user can fetch any object — IDOR. - ModelViewSet
update/destroyactions callhas_object_permissiononly onget_object(); if a custom action bypassesget_object, no check fires.
4. Mass assignment in serializers
ModelSerializerwithfields = '__all__'exposes every field includingis_staff,is_superuser.- Use
fields = (...)explicit list andread_only_fields = (...)for privileged ones.
5. Settings leaks / debug
DEBUG = Truein prod renders error page with full source, settings, request — leaks secret key.ALLOWED_HOSTS = ['*']allows host header attacks (host-header-injection).SECRET_KEYinsettings.pycommitted to git → session forgery, password reset poisoning.django-debug-toolbarinINSTALLED_APPSfor prod = SQL/template internals reachable ifINTERNAL_IPSmisconfigured.
6. Session / cache deserialization
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'+SESSION_ENGINEwritable storage = RCE if session signing key leaks.CACHES['default']['BACKEND'] = 'django_redis.serializers.pickle.PickleSerializer'— same.- Default since 1.6 is
JSONSerializer; flag any pickle override.
7. Open redirect via next
LOGIN_REDIRECT_URLoverridable vianextparam;is_safe_url/url_has_allowed_host_and_schemeis the gate. Custom flows often skip it.
8. SSRF
requests.get(model.url)with user-controlledurl. Combine withURLValidatorweakness — DNS rebinding (dns-rebinding).django-storageswith user-controlled bucket name → SSRF via S3 SDK.
9. File upload
FileField+MEDIA_ROOTserved by web server can render.html/.svg→ stored XSS.default_storage.save(name, content)does not sanitisename; path traversal possible.- See file-upload.
10. CSRF / CORS
CSRF_TRUSTED_ORIGINSwildcard or overly broad = same-site CSRF still works.django-cors-headersCORS_ALLOW_ALL_ORIGINS = True+CORS_ALLOW_CREDENTIALS = True= wildcard credentialed CORS (browser blocks, but old clients / non-browser tools don’t).
11. Admin exec surface
- Custom admin actions that call
eval/exec/os.systemwith model field data = RCE for any staff user. django-admin shellis dev-only; flag if exposed via a custom view.
Grep starter
1
2
3
4
5
6
7
rg -n '\.raw\(|\.extra\(' .
rg -n 'mark_safe|\|safe' --type=html
rg -n 'fields\s*=\s*[\x27"]__all__[\x27"]' -g 'serializers.py'
rg -n 'DEBUG\s*=\s*True|ALLOWED_HOSTS\s*=\s*\[\x27\*' settings*.py
rg -n 'SESSION_SERIALIZER|PickleSerializer'
rg -n 'requests\.(get|post)\(.*\.url|urllib\.request\.urlopen\(' .
rg -n 'SECRET_KEY\s*=\s*[\x27"]' settings*.py
Tooling
bandit— generic Python SAST.semgreppython.django.securityruleset.django-check-deploy(built-in:python manage.py check --deploy).safety/pip-auditfor dep CVEs.