Skip to content

Commit 23dd410

Browse files
authored
Merge pull request #2905 from graphite-project/copilot/fix-issue-2871
Fix open redirect vulnerability in account login/logout views
2 parents 4a11125 + 5782f4d commit 23dd410

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

webapp/graphite/account/views.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,28 @@
1919
from django.urls import reverse
2020
from django.http import HttpResponseRedirect
2121
from django.shortcuts import render
22+
from django.utils.http import url_has_allowed_host_and_scheme
2223
from graphite.user_util import getProfile, isAuthenticated
2324

2425

26+
def _get_safe_next_page(request, param_value):
27+
"""Return param_value if it is a safe local redirect target, otherwise the browser URL."""
28+
if url_has_allowed_host_and_scheme(
29+
url=param_value,
30+
allowed_hosts={request.get_host()},
31+
require_https=request.is_secure(),
32+
):
33+
return param_value
34+
return reverse('browser')
35+
36+
2537
def loginView(request):
2638
username = request.POST.get('username')
2739
password = request.POST.get('password')
2840
if request.method == 'GET':
29-
nextPage = request.GET.get('nextPage', reverse('browser'))
41+
nextPage = _get_safe_next_page(request, request.GET.get('nextPage', reverse('browser')))
3042
else:
31-
nextPage = request.POST.get('nextPage', reverse('browser'))
43+
nextPage = _get_safe_next_page(request, request.POST.get('nextPage', reverse('browser')))
3244
if username and password:
3345
user = authenticate(username=username,password=password)
3446
if user is None:
@@ -43,7 +55,7 @@ def loginView(request):
4355

4456

4557
def logoutView(request):
46-
nextPage = request.GET.get('nextPage', reverse('browser'))
58+
nextPage = _get_safe_next_page(request, request.GET.get('nextPage', reverse('browser')))
4759
logout(request)
4860
return HttpResponseRedirect(nextPage)
4961

@@ -60,5 +72,5 @@ def updateProfile(request):
6072
if profile:
6173
profile.advancedUI = request.POST.get('advancedUI','off') == 'on'
6274
profile.save()
63-
nextPage = request.POST.get('nextPage', reverse('browser'))
75+
nextPage = _get_safe_next_page(request, request.POST.get('nextPage', reverse('browser')))
6476
return HttpResponseRedirect(nextPage)

webapp/tests/test_account.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from django.contrib.auth.models import User
4+
from django.urls import reverse
5+
from .base import TestCase
6+
7+
8+
class LogoutViewTest(TestCase):
9+
def test_logout_default_redirect(self):
10+
"""Logout without nextPage redirects to the browser."""
11+
url = reverse('account_logout')
12+
response = self.client.get(url)
13+
self.assertEqual(response.status_code, 302)
14+
self.assertEqual(response['Location'], reverse('browser'))
15+
16+
def test_logout_safe_relative_redirect(self):
17+
"""Logout with a safe relative nextPage redirects there."""
18+
url = reverse('account_logout')
19+
response = self.client.get(url, {'nextPage': '/graphite/'})
20+
self.assertEqual(response.status_code, 302)
21+
self.assertEqual(response['Location'], '/graphite/')
22+
23+
def test_logout_open_redirect_blocked(self):
24+
"""Logout with an external nextPage falls back to the browser URL."""
25+
url = reverse('account_logout')
26+
response = self.client.get(url, {'nextPage': 'http://evil.example.com'})
27+
self.assertEqual(response.status_code, 302)
28+
self.assertEqual(response['Location'], reverse('browser'))
29+
30+
def test_logout_open_redirect_blocked_https(self):
31+
"""Logout with an external HTTPS nextPage falls back to the browser URL."""
32+
url = reverse('account_logout')
33+
response = self.client.get(url, {'nextPage': 'https://evil.example.com/path'})
34+
self.assertEqual(response.status_code, 302)
35+
self.assertEqual(response['Location'], reverse('browser'))
36+
37+
38+
class LoginViewTest(TestCase):
39+
def setUp(self):
40+
self.user = User.objects.create_user(
41+
username='testuser', password='testpass', is_active=True
42+
)
43+
44+
def test_login_open_redirect_blocked_get(self):
45+
"""GET login page with external nextPage falls back to the browser URL."""
46+
url = reverse('account_login')
47+
response = self.client.get(url, {'nextPage': 'http://evil.example.com'})
48+
self.assertEqual(response.status_code, 200)
49+
self.assertContains(response, reverse('browser'))
50+
51+
def test_login_open_redirect_blocked_post(self):
52+
"""POST login with external nextPage falls back to the browser URL."""
53+
url = reverse('account_login')
54+
response = self.client.post(url, {
55+
'username': 'testuser',
56+
'password': 'testpass',
57+
'nextPage': 'http://evil.example.com',
58+
})
59+
self.assertEqual(response.status_code, 302)
60+
self.assertEqual(response['Location'], reverse('browser'))
61+
62+
def test_login_safe_relative_redirect(self):
63+
"""POST login with a safe relative nextPage redirects there."""
64+
url = reverse('account_login')
65+
response = self.client.post(url, {
66+
'username': 'testuser',
67+
'password': 'testpass',
68+
'nextPage': '/graphite/',
69+
})
70+
self.assertEqual(response.status_code, 302)
71+
self.assertEqual(response['Location'], '/graphite/')

0 commit comments

Comments
 (0)