Rohan Yeole - Homepage Rohan Yeole

5 Django Security Bugs That AI Consistently Generates (And How to Fix Them)

May 9, 20261 min read

AI tools write Django code that runs. They don't reliably write Django code that's secure. The difference matters when real users and real data are involved.

These five vulnerabilities appear in nearly every AI-generated Django codebase I've audited. Each one can be introduced in a single line of plausible-looking code, and each one is exploitable without advanced knowledge.

1. Missing Object-Level Permission Checks (IDOR)

What AI generates:

@login_required
def get_invoice(request, invoice_id):
    invoice = Invoice.objects.get(id=invoice_id)
    return JsonResponse(invoice.to_dict())

This view checks that the user is logged in. It does not check that the invoice belongs to this user. Any authenticated user can enumerate invoice IDs and read invoices belonging to other customers.

This is called an Insecure Direct Object Reference (IDOR) — the OWASP #1 category for API vulnerabilities in 2026.

Fix:

@login_required
def get_invoice(request, invoice_id):
    invoice = get_object_or_404(Invoice, id=invoice_id, user=request.user)
    return JsonResponse(invoice.to_dict())

The user=request.user filter means a mismatched user gets a 404, not someone else's data. Apply this pattern to every view that retrieves a user-owned object: orders, profiles, documents, messages.

Where to look in your codebase: Any get_object_or_404 or .objects.get() call that uses a URL parameter. If it doesn't also filter by user=request.user (or a related ownership field), it's likely vulnerable.

2. Views That Forget @login_required

What AI generates:

AI generates views function by function. When you ask it to "add a delete endpoint," it creates a working delete view — but often without authentication, because you didn't specify it and it wasn't in the immediate context.

# No @login_required — anyone can call this
def delete_account(request, user_id):
    if request.method == "POST":
        User.objects.filter(id=user_id).delete()
        return JsonResponse({"status": "deleted"})

Fix:

@login_required
def delete_account(request, user_id):
    if request.method == "POST" and request.user.id == user_id:
        request.user.delete()
        return redirect("home")
    return HttpResponseForbidden()

Better fix — use Django's LoginRequiredMixin at the class level or protect entire URL namespaces:

# In urls.py — wrap all private views under an authenticated prefix
from django.contrib.auth.decorators import login_required
from django.urls import path

urlpatterns = [
    path("dashboard/", login_required(DashboardView.as_view())),
    path("account/delete/", login_required(DeleteAccountView.as_view())),
]

Audit method:grep -r "def " views.py | grep -v login_required won't catch everything, but a full permission audit means reading every view and confirming each has appropriate auth.

3. DEBUG = True in Production Settings

What AI generates:

AI writes settings files with DEBUG = True because that's what works during development. It may suggest a .env-based override pattern, but it doesn't enforce it. Many deployed apps reach production with DEBUG = True still hardcoded or as the fallback.

Why it's dangerous:

  • Exposes full stack traces to any visitor who triggers an error — including your database table names, file paths, and sometimes values from settings
  • Disables Django's security headers (HSTS, X-Frame-Options, etc.)
  • Reveals installed apps, middleware, and URL patterns to attackers

Fix:

# settings.py
import os
from pathlib import Path

DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"

if not DEBUG:
    ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
    SECURE_HSTS_SECONDS = 31536000
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True

Make DEBUG = True impossible unless explicitly set in the environment. The fallback must be False.

Verify in production:curl -s https://yourdomain.com/nonexistent-page/ | grep -i "django" — if you see Django's debug page HTML, DEBUG is True.

4. CSRF Missing on AJAX Endpoints

What AI generates:

When AI writes AJAX-friendly Django views, it often disables CSRF without explaining the risk:

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def api_submit(request):
    if request.method == "POST":
        data = json.loads(request.body)
        # process data...

@csrf_exempt turns off Django's Cross-Site Request Forgery protection. An attacker can embed a form on their site that silently submits to your endpoint using the victim's session cookie.

Fix for API endpoints that accept JSON:

# JSON APIs don't need the CSRF cookie if they require the X-CSRFToken header
# Fetch from your own frontend:

const response = await fetch("/api/submit/", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-CSRFToken": getCookie("csrftoken"),  // read from cookie
    },
    body: JSON.stringify(data),
});
# Django side — don't use @csrf_exempt; use @ensure_csrf_cookie on the page view
# and require the header on POST views
from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def app_page(request):
    return render(request, "app/index.html")

For public API endpoints used by third-party clients, use token-based auth (DRF's TokenAuthentication or JWT) instead of session cookies — then CSRF is not applicable.

5. Path Traversal in File Handling

What AI generates:

File upload and download code from AI often trusts user-supplied filenames:

import os

def download_file(request):
    filename = request.GET.get("file")
    file_path = os.path.join("/var/app/uploads", filename)
    with open(file_path, "rb") as f:
        return FileResponse(f)

An attacker passes file=../../etc/passwd and reads your server's password file. Or file=../../app/settings.py and gets your secret key and database credentials.

Fix:

import os
from pathlib import Path

UPLOAD_DIR = Path("/var/app/uploads").resolve()

def download_file(request):
    filename = request.GET.get("file", "")
    # Strip any directory components from the filename
    safe_name = os.path.basename(filename)
    file_path = (UPLOAD_DIR / safe_name).resolve()

    # Verify the resolved path is still inside UPLOAD_DIR
    if not str(file_path).startswith(str(UPLOAD_DIR)):
        return HttpResponseForbidden()

    if not file_path.exists():
        raise Http404

    return FileResponse(open(file_path, "rb"))

Better yet: store uploaded files by a UUID you generate, not by the user's filename. Then user-supplied names have no effect on what file is served.

What to Do If You've Already Deployed

  1. Immediately: Confirm DEBUG = False in production and rotate your SECRET_KEY if it was ever exposed
  2. Same day: Audit every view that accesses user-owned objects — add user=request.user filters
  3. This week: Add @login_required to every view that should be private; remove every @csrf_exempt
  4. Before next release: Add file path validation to any file upload/download handlers

These five issues are fixable in a day for a small app. What's not fixable in a day is recovering from a breach — customer data loss, regulatory consequences, and reputational damage.

If you want an independent security review of your AI-generated Django codebase, hire me for an AI code audit. I'll produce a prioritized report of every exploitable issue, with exact line references and fixes.

Chat with me on WhatsApp