Skip to content

Commit 9c0e365

Browse files
authored
Merge pull request #2906 from graphite-project/copilot/fix-issue-2872-again
Fix open redirect vulnerability in URL shortener via backslash bypass
2 parents 23dd410 + 6898b96 commit 9c0e365

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

webapp/graphite/url_shortener/views.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.urls import reverse
55
from django.shortcuts import get_object_or_404
66
from django.http import HttpResponse, HttpResponsePermanentRedirect
7+
from django.utils.http import url_has_allowed_host_and_scheme
78

89
from graphite.url_shortener.baseconv import base62
910
from graphite.url_shortener.models import Link
@@ -14,9 +15,15 @@ def follow(request, link_id):
1415
"""Follow existing links"""
1516
key = base62.to_decimal(link_id)
1617
link = get_object_or_404(Link, pk=key)
18+
browser_url = reverse('browser')
1719
# Strip leading slashes from the stored URL to prevent open redirect via
1820
# protocol-relative URLs (e.g. //evil.com) being used as redirect targets.
19-
url = reverse('browser') + link.url.lstrip('/')
21+
url = browser_url + link.url.lstrip('/')
22+
# Validate the resulting URL is safe and does not redirect to an external
23+
# domain (e.g. via backslash bypass: /\evil.com interpreted as //evil.com
24+
# by some browsers).
25+
if not url_has_allowed_host_and_scheme(url=url, allowed_hosts={request.get_host()}):
26+
url = browser_url
2027
return HttpResponsePermanentRedirect(url)
2128

2229

webapp/tests/test_url_shortener.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from urllib.parse import urlparse
77

8+
from django.utils.http import url_has_allowed_host_and_scheme
9+
810
from .base import TestCase
911

1012

@@ -43,3 +45,23 @@ def test_follow_open_redirect_prevention(self):
4345
parsed = urlparse(redirect_url)
4446
self.assertFalse(bool(parsed.netloc),
4547
'Open redirect to external domain detected: %s' % redirect_url)
48+
49+
def test_follow_open_redirect_backslash_prevention(self):
50+
"""Test that backslash-prefixed URLs cannot cause open redirects.
51+
52+
Some browsers interpret /\\evil.com as //evil.com (a protocol-relative
53+
URL pointing to evil.com). The follow() view must reject such URLs.
54+
"""
55+
shorten_url = reverse('shorten', kwargs={'path': '\\evil.com'})
56+
response = self.client.get(shorten_url)
57+
self.assertEqual(response.status_code, 200)
58+
short_path = response.content.decode('utf-8')
59+
60+
follow_response = self.client.get(short_path)
61+
self.assertEqual(follow_response.status_code, 301)
62+
redirect_url = follow_response['Location']
63+
# The redirect must be a safe internal URL
64+
self.assertTrue(
65+
url_has_allowed_host_and_scheme(url=redirect_url, allowed_hosts={'testserver'}),
66+
'Unsafe redirect detected: %s' % redirect_url,
67+
)

0 commit comments

Comments
 (0)