RSTR-CSRF-002 — Django @csrf_exempt on a state-changing view
Summary
A Django view is decorated with @csrf_exempt, removing the CSRF
protection that the rest of the project's middleware enforces. If the
view accepts POST / PUT / PATCH / DELETE, any cross-site form
submission targeting it succeeds with the victim's session cookies
attached.
Severity
Medium. The bug is real but limited to the single view; the rest of
the application is still protected.
Languages
Python.
What rastray flags
Any use of the csrf_exempt decorator from django.views.decorators.csrf:
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt # ← flagged
def transfer_funds(request):
...
@csrf_exempt # ← flagged
@require_POST
def webhook(request):
...
What rastray deliberately does not flag
- Class-based views protected by Django's default CSRF middleware.
- Views with
@requires_csrf_tokenor@ensure_csrf_cookie.
How to fix it
For internal endpoints: remove @csrf_exempt. Let the middleware
enforce the token; if the client is a SPA, expose the token via
{% csrf_token %} or ensure_csrf_cookie and send it back as
X-CSRFToken.
For third-party webhooks (Stripe, GitHub, Slack, etc.): verify the incoming signature instead of disabling CSRF. The signature does the job that the CSRF token does for browser-originated requests.
@csrf_exempt
@require_POST
def stripe_webhook(request):
sig = request.META.get('HTTP_STRIPE_SIGNATURE', '')
try:
event = stripe.Webhook.construct_event(
request.body, sig, settings.STRIPE_WEBHOOK_SECRET
)
except (ValueError, stripe.error.SignatureVerificationError):
return HttpResponseBadRequest()
...
The csrf_exempt is unavoidable in that case — suppress it with a
comment explaining the signature check substitutes for the missing
CSRF protection:
# rastray-ignore: RSTR-CSRF-002 — Stripe webhook; signature verified above
@csrf_exempt
@require_POST
def stripe_webhook(request): ...