Form submission after logging in can result in CSRF errors for documented reasons, and these errors are particularly common when using short session timeouts. This can lead to data loss in certain cases. If views could instead opt-in to handle CSRF errors, allowing the error to be treated for example as a form error, the view could rerender the form with the submitted data and a refreshed CSRF token, allowing the end user to reconfirm the submission without losing what they submitted. I implemented a proof-of-concept using a decorator called defer_csrf_failure
and a change to FormMixin.get_form
at the DjangoCon Europe sprints today and would be interested in any feedback.
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index 5ae1aae5c6..04b276beec 100644
--- a/django/middleware/csrf.py
+++ b/django/middleware/csrf.py
@@ -464,7 +464,11 @@ class CsrfViewMiddleware(MiddlewareMixin):
try:
self._check_token(request)
except RejectRequest as exc:
- return self._reject(request, exc.reason)
+ if getattr(callback, "defer_csrf_failure", False):
+ request.csrf_failure_handled = False
+ request.csrf_failure_reason = exc.reason
+ else:
+ return self._reject(request, exc.reason)
return self._accept(request)
@@ -480,4 +484,8 @@ class CsrfViewMiddleware(MiddlewareMixin):
# by custom middleware but before those subsequent calls.
request.META["CSRF_COOKIE_NEEDS_UPDATE"] = False
+ csrf_failure_handled = getattr(request, "csrf_failure_handled", None)
+ if csrf_failure_handled is False:
+ return self._reject(request, request.csrf_failure_reason)
+
return response
diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py
index 4b8b8804b0..14ca4a39c0 100644
--- a/django/views/decorators/csrf.py
+++ b/django/views/decorators/csrf.py
@@ -67,3 +67,21 @@ def csrf_exempt(view_func):
_view_wrapper.csrf_exempt = True
return wraps(view_func)(_view_wrapper)
+
+
+def defer_csrf_failure(view_func):
+ """Allow a view function to handle CSRF errors itself."""
+
+ if iscoroutinefunction(view_func):
+
+ async def _view_wrapper(request, *args, **kwargs):
+ return await view_func(request, *args, **kwargs)
+
+ else:
+
+ def _view_wrapper(request, *args, **kwargs):
+ return view_func(request, *args, **kwargs)
+
+ _view_wrapper.defer_csrf_failure = True
+
+ return wraps(view_func)(_view_wrapper)
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
index ebd071cf00..cd7f09e03c 100644
--- a/django/views/generic/edit.py
+++ b/django/views/generic/edit.py
@@ -34,7 +34,13 @@ class FormMixin(ContextMixin):
"""Return an instance of the form to be used in this view."""
if form_class is None:
form_class = self.get_form_class()
- return form_class(**self.get_form_kwargs())
+ form = form_class(**self.get_form_kwargs())
+ if getattr(self.dispatch, "defer_csrf_failure", False) and getattr(
+ self.request, "csrf_failure_reason", None
+ ):
+ form.add_error("__all__", self.request.csrf_failure_reason)
+ self.request.csrf_failure_handled = True
+ return form
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""