Skip to content

Commit 029756e

Browse files
authored
Account for CDN with csrf cookie generation (boostorg#1958) (boostorg#1963)
1 parent 387199d commit 029756e

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

libraries/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ def clean_email(self):
916916
ensure the email exists."""
917917
email = self.cleaned_data.get("email")
918918
commit_author_email = CommitAuthorEmail.objects.filter(
919-
email_iexact=email
919+
email__iexact=email
920920
).first()
921921
msg = None
922922

templates/includes/_header.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,4 @@
9494
guide.classList.remove("hidden");
9595
}
9696
}
97-
document.body.addEventListener('htmx:configRequest', function(event) {
98-
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
99-
});
10097
</script>

templates/users/profile.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,19 @@ <h3>{% trans 'Delete Account' %}</h3>
152152
</div>
153153

154154
{% include "modal.html" %}
155+
156+
<script>
157+
document.body.addEventListener('htmx:configRequest', function(event) {
158+
/* only set CSRF token if config-sessionid cookie exists.
159+
we don't really need this check while this functionality is limited to this
160+
page, but I'm adding it anyway in case we move this in the future so we don't
161+
overlook it. */
162+
const hasSessionId = document.cookie.split(';').some(function(cookie) {
163+
return cookie.trim().startsWith('config-sessionid=');
164+
});
165+
if (hasSessionId) {
166+
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
167+
}
168+
});
169+
</script>
155170
{% endblock %}

users/views.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,45 @@ def get_context_data(self, **kwargs):
310310
context["mobile"] = self.request.GET.get("ui")
311311
return context
312312

313+
def render_to_response(self, context, **response_kwargs):
314+
"""
315+
Override to delete CSRF cookie when session cookie is not present.
316+
This cleans up CSRF cookies for anonymous users.
317+
TODO: december 2025 - remove this override, cookies should have been cleared
318+
"""
319+
response = super().render_to_response(context, **response_kwargs)
320+
321+
session_cookie_name = settings.SESSION_COOKIE_NAME
322+
has_session = session_cookie_name in self.request.COOKIES
323+
has_csrf_cookie = "csrftoken" in self.request.COOKIES
324+
325+
# only delete CSRF cookie if user was previously logged in but session expired
326+
if (
327+
has_csrf_cookie
328+
and not has_session
329+
and self.request.session.session_key is None
330+
):
331+
# check if user is on pages that require CSRF but don't require login
332+
# (auth pages where anonymous users submit forms)
333+
referer = self.request.META.get("HTTP_REFERER", "")
334+
current_path = self.request.path
335+
336+
# paths that anonymous users can access and have forms
337+
anonymous_form_paths = [
338+
"/accounts/", # login, signup, password reset, email confirm, etc.
339+
"/socialaccount/", # social auth pages
340+
]
341+
342+
# don't delete if user is on or coming from anonymous form pages
343+
is_anonymous_form = any(
344+
path in referer for path in anonymous_form_paths
345+
) or any(path in current_path for path in anonymous_form_paths)
346+
347+
if not is_anonymous_form:
348+
response.delete_cookie("csrftoken", path="/")
349+
350+
return response
351+
313352

314353
class DeleteUserView(LoginRequiredMixin, FormView):
315354
template_name = "users/delete.html"

0 commit comments

Comments
 (0)