VYPR
High severityNVD Advisory· Published Nov 18, 2012· Updated Apr 29, 2026

CVE-2012-4520

CVE-2012-4520

Description

The django.http.HttpRequest.get_host function in Django 1.3.x before 1.3.4 and 1.4.x before 1.4.2 allows remote attackers to generate and display arbitrary URLs via crafted username and password Host header values.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
DjangoPyPI
>= 1.3, < 1.3.41.3.4
DjangoPyPI
>= 1.4, < 1.4.21.4.2

Affected products

8
  • cpe:2.3:a:djangoproject:django:1.3:*:*:*:*:*:*:*+ 7 more
    • cpe:2.3:a:djangoproject:django:1.3:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.3.1:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.3.2:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.3.3:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.3:alpha1:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.3:beta1:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.1:*:*:*:*:*:*:*

Patches

3
b45c377f8f48

Fixed a security issue related to password resets

https://github.com/django/djangoPreston HolmesOct 17, 2012via ghsa
4 files changed · +46 1
  • django/contrib/auth/tests/urls.py+1 0 modified
    @@ -19,6 +19,7 @@ def remote_user_auth_view(request):
         (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
         (r'^remote_user/$', remote_user_auth_view),
         (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
    +    (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)),
         (r'^login_required/$', login_required(password_reset)),
         (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
     )
    
  • django/contrib/auth/tests/views.py+39 0 modified
    @@ -9,6 +9,7 @@
     from django.contrib.auth.models import User
     from django.test import TestCase
     from django.core import mail
    +from django.core.exceptions import SuspiciousOperation
     from django.core.urlresolvers import reverse
     from django.http import QueryDict
     
    @@ -69,6 +70,44 @@ def test_email_found_custom_from(self):
             self.assertEqual(len(mail.outbox), 1)
             self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
     
    +    def test_admin_reset(self):
    +        "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override."
    +        response = self.client.post('/admin_password_reset/',
    +            {'email': 'staffmember@example.com'},
    +            HTTP_HOST='adminsite.com'
    +        )
    +        self.assertEqual(response.status_code, 302)
    +        self.assertEqual(len(mail.outbox), 1)
    +        self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
    +        self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
    +
    +    def test_poisoned_http_host(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails"
    +        # This attack is based on the way browsers handle URLs. The colon
    +        # should be used to separate the port, but if the URL contains an @,
    +        # the colon is interpreted as part of a username for login purposes,
    +        # making 'evil.com' the request domain. Since HTTP_HOST is used to
    +        # produce a meaningful reset URL, we need to be certain that the
    +        # HTTP_HOST header isn't poisoned. This is done as a check when get_host()
    +        # is invoked, but we check here as a practical consequence.
    +        def test_host_poisoning():
    +            self.client.post('/password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertRaises(SuspiciousOperation, test_host_poisoning)
    +        self.assertEqual(len(mail.outbox), 0)
    +
    +    def test_poisoned_http_host_admin_site(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
    +        def test_host_poisoning():
    +            self.client.post('/admin_password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertRaises(SuspiciousOperation, test_host_poisoning)
    +        self.assertEqual(len(mail.outbox), 0)
    +
         def _test_confirm_start(self):
             # Start by creating the email
             response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
    
  • django/contrib/auth/views.py+1 1 modified
    @@ -151,7 +151,7 @@ def password_reset(request, is_admin_site=False,
                     'request': request,
                 }
                 if is_admin_site:
    -                opts = dict(opts, domain_override=request.META['HTTP_HOST'])
    +                opts = dict(opts, domain_override=request.get_host())
                 form.save(**opts)
                 return HttpResponseRedirect(post_reset_redirect)
         else:
    
  • django/http/__init__.py+5 0 modified
    @@ -165,6 +165,11 @@ def get_host(self):
                 server_port = str(self.META['SERVER_PORT'])
                 if server_port != (self.is_secure() and '443' or '80'):
                     host = '%s:%s' % (host, server_port)
    +
    +        # Disallow potentially poisoned hostnames.
    +        if set(';/?@&=+$,').intersection(host):
    +            raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
    +
             return host
     
         def get_full_path(self):
    
92d3430f1217

Fixed a security issue related to password resets

https://github.com/django/djangoPreston HolmesOct 17, 2012via ghsa
4 files changed · +44 1
  • django/contrib/auth/tests/urls.py+1 0 modified
    @@ -51,6 +51,7 @@ def userpage(request):
         (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
         (r'^remote_user/$', remote_user_auth_view),
         (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
    +    (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)),
         (r'^login_required/$', login_required(password_reset)),
         (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
     
    
  • django/contrib/auth/tests/views.py+37 0 modified
    @@ -7,6 +7,7 @@
     from django.contrib.sites.models import Site, RequestSite
     from django.contrib.auth.models import User
     from django.core import mail
    +from django.core.exceptions import SuspiciousOperation
     from django.core.urlresolvers import reverse, NoReverseMatch
     from django.http import QueryDict
     from django.utils.encoding import force_unicode
    @@ -106,6 +107,42 @@ def test_email_found_custom_from(self):
             self.assertEqual(len(mail.outbox), 1)
             self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
     
    +    def test_admin_reset(self):
    +        "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override."
    +        response = self.client.post('/admin_password_reset/',
    +            {'email': 'staffmember@example.com'},
    +            HTTP_HOST='adminsite.com'
    +        )
    +        self.assertEqual(response.status_code, 302)
    +        self.assertEqual(len(mail.outbox), 1)
    +        self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
    +        self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
    +
    +    def test_poisoned_http_host(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails"
    +        # This attack is based on the way browsers handle URLs. The colon
    +        # should be used to separate the port, but if the URL contains an @,
    +        # the colon is interpreted as part of a username for login purposes,
    +        # making 'evil.com' the request domain. Since HTTP_HOST is used to
    +        # produce a meaningful reset URL, we need to be certain that the
    +        # HTTP_HOST header isn't poisoned. This is done as a check when get_host()
    +        # is invoked, but we check here as a practical consequence.
    +        with self.assertRaises(SuspiciousOperation):
    +            self.client.post('/password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertEqual(len(mail.outbox), 0)
    +
    +    def test_poisoned_http_host_admin_site(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
    +        with self.assertRaises(SuspiciousOperation):
    +            self.client.post('/admin_password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertEqual(len(mail.outbox), 0)
    +
         def _test_confirm_start(self):
             # Start by creating the email
             response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
    
  • django/contrib/auth/views.py+1 1 modified
    @@ -156,7 +156,7 @@ def password_reset(request, is_admin_site=False,
                     'request': request,
                 }
                 if is_admin_site:
    -                opts = dict(opts, domain_override=request.META['HTTP_HOST'])
    +                opts = dict(opts, domain_override=request.get_host())
                 form.save(**opts)
                 return HttpResponseRedirect(post_reset_redirect)
         else:
    
  • django/http/__init__.py+5 0 modified
    @@ -212,6 +212,11 @@ def get_host(self):
                 server_port = str(self.META['SERVER_PORT'])
                 if server_port != (self.is_secure() and '443' or '80'):
                     host = '%s:%s' % (host, server_port)
    +
    +        # Disallow potentially poisoned hostnames.
    +        if set(';/?@&=+$,').intersection(host):
    +            raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
    +
             return host
     
         def get_full_path(self):
    
9305c0e12d43

Fixed a security issue related to password resets

https://github.com/django/djangoPreston HolmesOct 17, 2012via ghsa
4 files changed · +44 1
  • django/contrib/auth/tests/urls.py+1 0 modified
    @@ -55,6 +55,7 @@ def userpage(request):
         (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
         (r'^remote_user/$', remote_user_auth_view),
         (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
    +    (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)),
         (r'^login_required/$', login_required(password_reset)),
         (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
     
    
  • django/contrib/auth/tests/views.py+37 0 modified
    @@ -5,6 +5,7 @@
     from django.contrib.sites.models import Site, RequestSite
     from django.contrib.auth.models import User
     from django.core import mail
    +from django.core.exceptions import SuspiciousOperation
     from django.core.urlresolvers import reverse, NoReverseMatch
     from django.http import QueryDict
     from django.utils.encoding import force_text
    @@ -103,6 +104,42 @@ def test_email_found_custom_from(self):
             self.assertEqual(len(mail.outbox), 1)
             self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
     
    +    def test_admin_reset(self):
    +        "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override."
    +        response = self.client.post('/admin_password_reset/',
    +            {'email': 'staffmember@example.com'},
    +            HTTP_HOST='adminsite.com'
    +        )
    +        self.assertEqual(response.status_code, 302)
    +        self.assertEqual(len(mail.outbox), 1)
    +        self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
    +        self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
    +
    +    def test_poisoned_http_host(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails"
    +        # This attack is based on the way browsers handle URLs. The colon
    +        # should be used to separate the port, but if the URL contains an @,
    +        # the colon is interpreted as part of a username for login purposes,
    +        # making 'evil.com' the request domain. Since HTTP_HOST is used to
    +        # produce a meaningful reset URL, we need to be certain that the
    +        # HTTP_HOST header isn't poisoned. This is done as a check when get_host()
    +        # is invoked, but we check here as a practical consequence.
    +        with self.assertRaises(SuspiciousOperation):
    +            self.client.post('/password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertEqual(len(mail.outbox), 0)
    +
    +    def test_poisoned_http_host_admin_site(self):
    +        "Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
    +        with self.assertRaises(SuspiciousOperation):
    +            self.client.post('/admin_password_reset/',
    +                {'email': 'staffmember@example.com'},
    +                HTTP_HOST='www.example:dr.frankenstein@evil.tld'
    +            )
    +        self.assertEqual(len(mail.outbox), 0)
    +
         def _test_confirm_start(self):
             # Start by creating the email
             response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
    
  • django/contrib/auth/views.py+1 1 modified
    @@ -163,7 +163,7 @@ def password_reset(request, is_admin_site=False,
                     'request': request,
                 }
                 if is_admin_site:
    -                opts = dict(opts, domain_override=request.META['HTTP_HOST'])
    +                opts = dict(opts, domain_override=request.get_host())
                 form.save(**opts)
                 return HttpResponseRedirect(post_reset_redirect)
         else:
    
  • django/http/__init__.py+5 0 modified
    @@ -180,6 +180,11 @@ def get_host(self):
                 server_port = str(self.META['SERVER_PORT'])
                 if server_port != ('443' if self.is_secure() else '80'):
                     host = '%s:%s' % (host, server_port)
    +
    +        # Disallow potentially poisoned hostnames.
    +        if set(';/?@&=+$,').intersection(host):
    +            raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
    +
             return host
     
         def get_full_path(self):
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

30

News mentions

0

No linked articles in our index yet.