diff --git a/django/http/__init__.py b/django/http/__init__.py index 6f9d70eebd9..19b581f5cb2 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -11,10 +11,10 @@ import warnings from io import BytesIO from pprint import pformat try: - from urllib.parse import quote, parse_qsl, urlencode, urljoin + from urllib.parse import quote, parse_qsl, urlencode, urljoin, urlparse except ImportError: # Python 2 from urllib import quote, urlencode - from urlparse import parse_qsl, urljoin + from urlparse import parse_qsl, urljoin, urlparse from django.utils.six.moves import http_cookies # Some versions of Python 2.7 and later won't need this encoding bug fix: @@ -80,7 +80,7 @@ else: from django.conf import settings from django.core import signing -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.core.files import uploadhandler from django.http.multipartparser import MultiPartParser from django.http.utils import * @@ -689,20 +689,22 @@ class HttpResponse(object): raise Exception("This %s instance cannot tell its position" % self.__class__) return sum([len(chunk) for chunk in self]) -class HttpResponseRedirect(HttpResponse): +class HttpResponseRedirectBase(HttpResponse): + allowed_schemes = ['http', 'https', 'ftp'] + + def __init__(self, redirect_to): + parsed = urlparse(redirect_to) + if parsed.scheme and parsed.scheme not in self.allowed_schemes: + raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) + super(HttpResponseRedirectBase, self).__init__() + self['Location'] = iri_to_uri(redirect_to) + +class HttpResponseRedirect(HttpResponseRedirectBase): status_code = 302 - def __init__(self, redirect_to): - super(HttpResponseRedirect, self).__init__() - self['Location'] = iri_to_uri(redirect_to) - -class HttpResponsePermanentRedirect(HttpResponse): +class HttpResponsePermanentRedirect(HttpResponseRedirectBase): status_code = 301 - def __init__(self, redirect_to): - super(HttpResponsePermanentRedirect, self).__init__() - self['Location'] = iri_to_uri(redirect_to) - class HttpResponseNotModified(HttpResponse): status_code = 304 diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 9b599db6d0e..0f61c2d8402 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -4,8 +4,11 @@ from __future__ import unicode_literals import copy import pickle -from django.http import (QueryDict, HttpResponse, SimpleCookie, BadHeaderError, - parse_cookie) +from django.core.exceptions import SuspiciousOperation +from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, + HttpResponsePermanentRedirect, + SimpleCookie, BadHeaderError, + parse_cookie) from django.utils import unittest @@ -309,6 +312,18 @@ class HttpResponseTests(unittest.TestCase): r = HttpResponse(['abc']) self.assertRaises(Exception, r.write, 'def') + def test_unsafe_redirect(self): + bad_urls = [ + 'data:text/html,', + 'mailto:test@example.com', + 'file:///etc/passwd', + ] + for url in bad_urls: + self.assertRaises(SuspiciousOperation, + HttpResponseRedirect, url) + self.assertRaises(SuspiciousOperation, + HttpResponsePermanentRedirect, url) + class CookieTests(unittest.TestCase): def test_encode(self):