Coreframe Labs

Automated security audit

Security audits for Django & DRF

Django's defaults are genuinely good — CSRF, escaping and parameterization are on unless you opt out. Which means Django vulnerabilities are almost always the places where someone opted out, usually for a good reason at the time.

Check a file now — free

Paste one file. The free tier runs a single security pass and returns up to three findings, each with the line of code that evidences it. Nothing is stored, nothing is used for training, and detected secrets are redacted before analysis.

Paste one file0 / 400 lines

Not stored. Not trained on. Secrets redacted before analysis.

What we find most often in Django & DRF

Each of these is a real pattern, with the code that causes it and the change that fixes it.

1. String interpolation into raw SQL

Symptom
Added for a query the ORM couldn't express cleanly. Works perfectly and is fully injectable.

The pattern

cursor.execute(f"SELECT * FROM orders WHERE status = '{status}' ORDER BY {sort}")

Why it bites
The f-string builds SQL before the driver sees it, so the driver cannot distinguish data from syntax. A status of `' OR '1'='1` returns every row; the sort parameter is worse, since it can carry an entire subquery.

The fix

# Parameterize values — the driver handles quoting and typing.
cursor.execute("SELECT * FROM orders WHERE status = %s", [status])

# Identifiers can't be parameterized. Allowlist them:
SORTABLE = {"created_at", "total", "status"}
if sort not in SORTABLE:
    raise ValidationError("Invalid sort field")
cursor.execute(f"SELECT * FROM orders WHERE status = %s ORDER BY {sort}", [status])

2. Mass assignment through `fields = "__all__"`

Symptom
A profile form lets a user set any column on the model, including the ones the form doesn't render.

The pattern

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = "__all__"   # includes is_staff, credit_balance, role

Why it bites
The form accepts every model field regardless of what the template renders. A crafted POST sets `role=admin` or `credit_balance=99999`, because validation is driven by the field list, not by the HTML.

The fix

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ["display_name", "bio", "avatar"]   # explicit allowlist

3. DRF viewsets with no permission class

Symptom
The endpoint is authenticated, so it looks protected. Any authenticated user can read and modify any object.

The pattern

class InvoiceViewSet(viewsets.ModelViewSet):
    queryset = Invoice.objects.all()
    serializer_class = InvoiceSerializer

Why it bites
`queryset = Invoice.objects.all()` is the entire authorization model here: authentication proves who you are and nothing scopes what you can reach. Any logged-in user can GET or DELETE any invoice by ID.

The fix

class InvoiceViewSet(viewsets.ModelViewSet):
    serializer_class = InvoiceSerializer
    permission_classes = [IsAuthenticated, IsOrgMember]

    def get_queryset(self):
        # Scope at the queryset — the object can never be addressed at all.
        return Invoice.objects.filter(org=self.request.user.org)

An automated pass reads one file. An audit reads the system.

The tool above is deliberately narrow — one file, one dimension, one pass. The issues that actually take systems down live between files: in the trust boundary nobody documented, the query pattern that only emerges at scale, the cache that was correct until the second tenant arrived. That is what the Coreframe Technical Audit is for — five business days, a written report, a live findings call, and a remediation proposal you can execute with or without us.

£1,500–£2,500 solo · £3,000–£5,000 funded startup · £3,000–£6,000 SME

See what the full audit covers →

Related audits

All stack audits →