Skip to content

Commit fcfe371

Browse files
committed
fix(auth): Prevent XSS injection in registration flow
1 parent 293b3b3 commit fcfe371

File tree

2 files changed

+55
-30
lines changed

2 files changed

+55
-30
lines changed

cl/users/tests.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,41 @@ def test_creating_a_new_user(self):
4343
host=self.live_server_url,
4444
)
4545

46-
def test_no_open_redirect(self):
47-
"""Prevent open redirects, which can cause spoofing attacks"""
48-
response = self.client.get(
49-
"{host}{path}".format(
46+
def test_redirects(self):
47+
"""Do we allow good redirects while banning bad ones?"""
48+
next_params = [
49+
# No open redirects (to a domain outside CL)
50+
("https://evil.com&email=e%40e.net", True),
51+
# No javascript (!)
52+
("javascript:confirm(document.domain)", True),
53+
# No spaces
54+
("/test test", True),
55+
# A safe redirect
56+
(reverse("about"), False),
57+
]
58+
for next_param, is_evil in next_params:
59+
bad_url = "{host}{path}?next={next}".format(
5060
host=self.live_server_url,
51-
path="/register/success/?next=https://evil.com&email=e%40e.net",
61+
path=reverse("register_success"),
62+
next=next_param,
5263
)
53-
)
54-
self.assertNotIn(
55-
"evil.com",
56-
response.content,
57-
msg="evil.com found in HTML of response. This suggests it was not"
58-
"cleaned by the sanitize_redirection function.",
59-
)
64+
print("Checking redirect on %s" % bad_url)
65+
response = self.client.get(bad_url)
66+
if is_evil:
67+
self.assertNotIn(
68+
next_param,
69+
response.content.decode("utf-8"),
70+
msg="'%s' found in HTML of response. This suggests it was "
71+
"not cleaned by the sanitize_redirection function."
72+
% next_param,
73+
)
74+
else:
75+
self.assertIn(
76+
next_param,
77+
response.content.decode("utf-8"),
78+
msg="'%s' not found in HTML of response. This suggests it "
79+
"was sanitized when it should not have been." % next_param,
80+
)
6081

6182
def test_signing_in(self):
6283
"""Can we create a user on the backend then sign them in"""

cl/users/utils.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
from django.contrib import messages
66
from django.contrib.auth.models import User
77
from django.db import transaction
8+
from django.urls import reverse
89

910
from cl.lib.crypto import md5
1011
from cl.users.models import UserProfile
1112

13+
from django.utils.http import is_safe_url
14+
1215

1316
def sanitize_redirection(request):
1417
"""Security and sanity checks on redirections.
1518
16-
Much of this code was grabbed from Django. The basic idea here is to:
19+
Much of this code was grabbed from Django:
20+
1721
1. Prevent open redirect attacks. Imagine getting an email:
1822
1923
Subject: Your CourtListener account is conformed
@@ -22,28 +26,28 @@ def sanitize_redirection(request):
2226
Without proper redirect sanitation, a user might click that link, and get
2327
redirected to cortlistener.com, which could be a spoof of the real thing.
2428
25-
2. Prevent illogical redirects. Like, don't let people redirect back to
26-
the sign-in page, for example.
29+
1. Prevent illogical redirects. Like, don't let people redirect back to
30+
the sign-in or register page.
31+
32+
1. Prevent garbage URLs (like empty ones or ones with spaces)
33+
34+
1. Prevent dangerous URLs (like JavaScript)
2735
2836
:return: Either the value requested or the default LOGIN_REDIRECT_URL, if
2937
a sanity or security check failed.
3038
"""
3139
redirect_to = request.GET.get("next", "")
32-
if "sign-in" in redirect_to:
33-
# thus, we don't redirect people back to the sign-in form
34-
redirect_to = ""
35-
36-
# Light security check -- make sure redirect_to isn't garbage.
37-
if not redirect_to or " " in redirect_to:
38-
redirect_to = settings.LOGIN_REDIRECT_URL
39-
40-
# Heavier security check -- redirects to http://example.com should
41-
# not be allowed, but things like /view/?param=http://example.com
42-
# should be allowed. This regex checks if there is a '//' *before* a
43-
# question mark.
44-
elif "//" in redirect_to and re.match(r"[^?]*//", redirect_to):
45-
redirect_to = settings.LOGIN_REDIRECT_URL
46-
40+
sign_in_url = reverse("sign-in") in redirect_to
41+
register_in_url = reverse("register") in redirect_to
42+
garbage_url = " " in redirect_to
43+
no_url = not redirect_to
44+
not_safe_url = not is_safe_url(
45+
redirect_to,
46+
allowed_hosts={request.get_host()},
47+
require_https=request.is_secure(),
48+
)
49+
if any([sign_in_url, register_in_url, garbage_url, no_url, not_safe_url]):
50+
return settings.LOGIN_REDIRECT_URL
4751
return redirect_to
4852

4953

0 commit comments

Comments
 (0)