Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Upgrade to Django 1.3.6.

  • Loading branch information...
commit 2c14d3d701b274e43c9f16304bfe395b4767bd8e 1 parent 93bff4b
@Osmose Osmose authored
Showing with 719 additions and 201 deletions.
  1. +0 −4 packages/Django/.gitignore
  2. +0 −6 packages/Django/.hgignore
  3. +0 −82 packages/Django/.tx/config
  4. +1 −1  packages/Django/django/__init__.py
  5. 0  packages/Django/django/bin/daily_cleanup.py
  6. 0  packages/Django/django/bin/django-admin.py
  7. 0  packages/Django/django/bin/unique-messages.py
  8. +4 −0 packages/Django/django/conf/global_settings.py
  9. 0  packages/Django/django/conf/project_template/manage.py
  10. +4 −0 packages/Django/django/conf/project_template/settings.py
  11. +8 −2 packages/Django/django/contrib/admin/options.py
  12. +22 −28 packages/Django/django/contrib/auth/views.py
  13. +4 −7 packages/Django/django/contrib/comments/views/comments.py
  14. +4 −3 packages/Django/django/contrib/comments/views/moderation.py
  15. +6 −4 packages/Django/django/contrib/comments/views/utils.py
  16. 0  packages/Django/django/contrib/gis/tests/data/texas.dbf
  17. +93 −1 packages/Django/django/core/serializers/xml_serializer.py
  18. +10 −2 packages/Django/django/forms/formsets.py
  19. +51 −5 packages/Django/django/http/__init__.py
  20. +6 −0 packages/Django/django/test/utils.py
  21. +12 −0 packages/Django/django/utils/http.py
  22. +7 −5 packages/Django/django/views/i18n.py
  23. +2 −2 packages/Django/docs/conf.py
  24. 0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.3.sh
  25. 0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.4.sh
  26. 0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.5.sh
  27. 0  packages/Django/docs/ref/contrib/gis/create_template_postgis-debian.sh
  28. +36 −0 packages/Django/docs/ref/settings.txt
  29. +1 −0  packages/Django/docs/releases/index.txt
  30. +4 −2 packages/Django/docs/topics/forms/formsets.txt
  31. +2 −2 packages/Django/docs/topics/forms/modelforms.txt
  32. 0  packages/Django/extras/csrf_migration_helper.py
  33. 0  packages/Django/extras/django_bash_completion
  34. +1 −1  packages/Django/setup.py
  35. +40 −0 packages/Django/tests/regressiontests/admin_views/tests.py
  36. 0  packages/Django/tests/regressiontests/app_loading/eggs/brokenapp.egg
  37. 0  packages/Django/tests/regressiontests/app_loading/eggs/modelapp.egg
  38. 0  packages/Django/tests/regressiontests/app_loading/eggs/nomodelapp.egg
  39. 0  packages/Django/tests/regressiontests/app_loading/eggs/omelet.egg
  40. +7 −0 packages/Django/tests/regressiontests/comment_tests/tests/comment_view_tests.py
  41. +85 −4 packages/Django/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
  42. +72 −13 packages/Django/tests/regressiontests/forms/tests/formsets.py
  43. +127 −26 packages/Django/tests/regressiontests/requests/tests.py
  44. +15 −0 packages/Django/tests/regressiontests/serializers_regress/tests.py
  45. 0  packages/Django/tests/regressiontests/templates/eggs/tagsegg.egg
  46. 0  packages/Django/tests/regressiontests/templates/templates/test_extends_error.html
  47. +16 −1 packages/Django/tests/regressiontests/views/tests/i18n.py
  48. 0  packages/Django/tests/runtests.py
  49. +79 −0 packages/django/docs/releases/1.3.6.txt
View
4 packages/Django/.gitignore
@@ -1,4 +0,0 @@
-*.egg-info
-*.pot
-*.py[co]
-docs/_build/
View
6 packages/Django/.hgignore
@@ -1,6 +0,0 @@
-syntax:glob
-
-*.egg-info
-*.pot
-*.py[co]
-docs/_build/
View
82 packages/Django/.tx/config
@@ -1,82 +0,0 @@
-[main]
-host = https://www.transifex.net
-
-[django.core]
-file_filter = django/conf/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/conf/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-admin]
-file_filter = django/contrib/admin/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/admin/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-admin-js]
-file_filter = django/contrib/admin/locale/<lang>/LC_MESSAGES/djangojs.po
-source_file = django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
-source_lang = en
-
-[django.contrib-admindocs]
-file_filter = django/contrib/admindocs/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/admindocs/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-auth]
-file_filter = django/contrib/auth/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/auth/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-comments]
-file_filter = django/contrib/comments/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/comments/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-contenttypes]
-file_filter = django/contrib/contenttypes/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-flatpages]
-file_filter = django/contrib/flatpages/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/flatpages/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-formtools]
-file_filter = django/contrib/formtools/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/formtools/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-gis]
-file_filter = django/contrib/gis/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/gis/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-humanize]
-file_filter = django/contrib/humanize/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/humanize/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-localflavor]
-file_filter = django/contrib/localflavor/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/localflavor/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-messages]
-file_filter = django/contrib/messages/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/messages/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-redirects]
-file_filter = django/contrib/redirects/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/redirects/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-sessions]
-file_filter = django/contrib/sessions/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/sessions/locale/en/LC_MESSAGES/django.po
-source_lang = en
-
-[django.contrib-sites]
-file_filter = django/contrib/sites/locale/<lang>/LC_MESSAGES/django.po
-source_file = django/contrib/sites/locale/en/LC_MESSAGES/django.po
-source_lang = en
View
2  packages/Django/django/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (1, 3, 4, 'final', 0)
+VERSION = (1, 3, 6, 'final', 0)
def get_version():
version = '%s.%s' % (VERSION[0], VERSION[1])
View
0  packages/Django/django/bin/daily_cleanup.py 100755 → 100644
File mode changed
View
0  packages/Django/django/bin/django-admin.py 100755 → 100644
File mode changed
View
0  packages/Django/django/bin/unique-messages.py 100755 → 100644
File mode changed
View
4 packages/Django/django/conf/global_settings.py
@@ -29,6 +29,10 @@
# * Receive x-headers
INTERNAL_IPS = ()
+# Hosts/domain names that are valid for this site.
+# "*" matches anything, ".example.com" matches example.com and all subdomains
+ALLOWED_HOSTS = ['*']
+
# Local time zone for this installation. All choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
# systems may support all possibilities).
View
0  packages/Django/django/conf/project_template/manage.py 100755 → 100644
File mode changed
View
4 packages/Django/django/conf/project_template/settings.py
@@ -20,6 +20,10 @@
}
}
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
View
10 packages/Django/django/contrib/admin/options.py
@@ -1242,15 +1242,21 @@ def delete_view(self, request, object_id, extra_context=None):
def history_view(self, request, object_id, extra_context=None):
"The 'history' admin view for this model."
from django.contrib.admin.models import LogEntry
+ # First check if the user can see this history.
model = self.model
+ obj = get_object_or_404(model, pk=unquote(object_id))
+
+ if not self.has_change_permission(request, obj):
+ raise PermissionDenied
+
+ # Then get the history for this object.
opts = model._meta
app_label = opts.app_label
action_list = LogEntry.objects.filter(
object_id = object_id,
content_type__id__exact = ContentType.objects.get_for_model(model).id
).select_related().order_by('action_time')
- # If no history was found, see whether this object even exists.
- obj = get_object_or_404(model, pk=unquote(object_id))
+
context = {
'title': _('Change history: %s') % force_unicode(obj),
'action_list': action_list,
View
50 packages/Django/django/contrib/auth/views.py
@@ -5,7 +5,7 @@
from django.http import HttpResponseRedirect, QueryDict
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.utils.http import base36_to_int
+from django.utils.http import base36_to_int, is_safe_url
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
@@ -33,18 +33,11 @@ def login(request, template_name='registration/login.html',
if request.method == "POST":
form = authentication_form(data=request.POST)
if form.is_valid():
- netloc = urlparse.urlparse(redirect_to)[1]
-
- # Use default setting if redirect_to is empty
- if not redirect_to:
- redirect_to = settings.LOGIN_REDIRECT_URL
-
- # Security check -- don't allow redirection to a different
- # host.
- elif netloc and netloc != request.get_host():
+ # Ensure the user-originating redirection url is safe.
+ if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = settings.LOGIN_REDIRECT_URL
- # Okay, security checks complete. Log the user in.
+ # Okay, security check complete. Log the user in.
auth_login(request, form.get_user())
if request.session.test_cookie_worked():
@@ -76,26 +69,27 @@ def logout(request, next_page=None,
Logs out the user and displays 'You are logged out' message.
"""
auth_logout(request)
- redirect_to = request.REQUEST.get(redirect_field_name, '')
- if redirect_to:
- netloc = urlparse.urlparse(redirect_to)[1]
+
+ if redirect_field_name in request.REQUEST:
+ next_page = request.REQUEST[redirect_field_name]
# Security check -- don't allow redirection to a different host.
- if not (netloc and netloc != request.get_host()):
- return HttpResponseRedirect(redirect_to)
+ if not is_safe_url(url=next_page, host=request.get_host()):
+ next_page = request.path
- if next_page is None:
- current_site = get_current_site(request)
- context = {
- 'site': current_site,
- 'site_name': current_site.name,
- 'title': _('Logged out')
- }
- context.update(extra_context or {})
- return render_to_response(template_name, context,
- context_instance=RequestContext(request, current_app=current_app))
- else:
+ if next_page:
# Redirect to this page until the session has been cleared.
- return HttpResponseRedirect(next_page or request.path)
+ return HttpResponseRedirect(next_page)
+
+ current_site = get_current_site(request)
+ context = {
+ 'site': current_site,
+ 'site_name': current_site.name,
+ 'title': _('Logged out')
+ }
+ if extra_context is not None:
+ context.update(extra_context)
+ return render_to_response(template_name, context,
+ context_instance=RequestContext(request, current_app=current_app))
def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
"""
View
11 packages/Django/django/contrib/comments/views/comments.py
@@ -40,9 +40,6 @@ def post_comment(request, next=None, using=None):
if not data.get('email', ''):
data["email"] = request.user.email
- # Check to see if the POST data overrides the view's next argument.
- next = data.get("next", next)
-
# Look up the object we're trying to comment about
ctype = data.get("content_type")
object_pk = data.get("object_pk")
@@ -94,9 +91,9 @@ def post_comment(request, next=None, using=None):
]
return render_to_response(
template_list, {
- "comment" : form.data.get("comment", ""),
- "form" : form,
- "next": next,
+ "comment": form.data.get("comment", ""),
+ "form": form,
+ "next": data.get("next", next),
},
RequestContext(request, {})
)
@@ -127,7 +124,7 @@ def post_comment(request, next=None, using=None):
request = request
)
- return next_redirect(data, next, comment_done, c=comment._get_pk_val())
+ return next_redirect(request, next, comment_done, c=comment._get_pk_val())
comment_done = confirmation_view(
template = "comments/posted.html",
View
7 packages/Django/django/contrib/comments/views/moderation.py
@@ -7,6 +7,7 @@
from django.contrib.comments import signals
from django.views.decorators.csrf import csrf_protect
+
@csrf_protect
@login_required
def flag(request, comment_id, next=None):
@@ -23,7 +24,7 @@ def flag(request, comment_id, next=None):
# Flag on POST
if request.method == 'POST':
perform_flag(request, comment)
- return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
+ return next_redirect(request, next, flag_done, c=comment.pk)
# Render a form on GET
else:
@@ -50,7 +51,7 @@ def delete(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as deleted instead of actually deleting it.
perform_delete(request, comment)
- return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
+ return next_redirect(request, next, delete_done, c=comment.pk)
# Render a form on GET
else:
@@ -77,7 +78,7 @@ def approve(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as approved.
perform_approve(request, comment)
- return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
+ return next_redirect(request, next, approve_done, c=comment.pk)
# Render a form on GET
else:
View
10 packages/Django/django/contrib/comments/views/utils.py
@@ -4,14 +4,15 @@
import urllib
import textwrap
-from django.http import HttpResponseRedirect
from django.core import urlresolvers
+from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.core.exceptions import ObjectDoesNotExist
from django.contrib import comments
+from django.utils.http import is_safe_url
-def next_redirect(data, default, default_view, **get_kwargs):
+def next_redirect(request, default, default_view, **get_kwargs):
"""
Handle the "where should I go next?" part of comment views.
@@ -21,9 +22,10 @@ def next_redirect(data, default, default_view, **get_kwargs):
Returns an ``HttpResponseRedirect``.
"""
- next = data.get("next", default)
- if next is None:
+ next = request.POST.get('next', default)
+ if not is_safe_url(url=next, host=request.get_host()):
next = urlresolvers.reverse(default_view)
+
if get_kwargs:
if '#' in next:
tmp = next.rsplit('#', 1)
View
0  packages/Django/django/contrib/gis/tests/data/texas.dbf 100755 → 100644
File mode changed
View
94 packages/Django/django/core/serializers/xml_serializer.py
@@ -8,6 +8,8 @@
from django.utils.xmlutils import SimplerXMLGenerator
from django.utils.encoding import smart_unicode
from xml.dom import pulldom
+from xml.sax import handler
+from xml.sax.expatreader import ExpatParser as _ExpatParser
class Serializer(base.Serializer):
"""
@@ -154,9 +156,13 @@ class Deserializer(base.Deserializer):
def __init__(self, stream_or_string, **options):
super(Deserializer, self).__init__(stream_or_string, **options)
- self.event_stream = pulldom.parse(self.stream)
+ self.event_stream = pulldom.parse(self.stream, self._make_parser())
self.db = options.pop('using', DEFAULT_DB_ALIAS)
+ def _make_parser(self):
+ """Create a hardened XML parser (no custom/external entities)."""
+ return DefusedExpatParser()
+
def next(self):
for event, node in self.event_stream:
if event == "START_ELEMENT" and node.nodeName == "object":
@@ -295,3 +301,89 @@ def getInnerText(node):
else:
pass
return u"".join(inner_text)
+
+
+# Below code based on Christian Heimes' defusedxml
+
+
+class DefusedExpatParser(_ExpatParser):
+ """
+ An expat parser hardened against XML bomb attacks.
+
+ Forbids DTDs, external entity references
+
+ """
+ def __init__(self, *args, **kwargs):
+ _ExpatParser.__init__(self, *args, **kwargs)
+ self.setFeature(handler.feature_external_ges, False)
+ self.setFeature(handler.feature_external_pes, False)
+
+ def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
+ raise DTDForbidden(name, sysid, pubid)
+
+ def entity_decl(self, name, is_parameter_entity, value, base,
+ sysid, pubid, notation_name):
+ raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
+
+ def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
+ # expat 1.2
+ raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
+
+ def external_entity_ref_handler(self, context, base, sysid, pubid):
+ raise ExternalReferenceForbidden(context, base, sysid, pubid)
+
+ def reset(self):
+ _ExpatParser.reset(self)
+ parser = self._parser
+ parser.StartDoctypeDeclHandler = self.start_doctype_decl
+ parser.EntityDeclHandler = self.entity_decl
+ parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
+ parser.ExternalEntityRefHandler = self.external_entity_ref_handler
+
+
+class DefusedXmlException(ValueError):
+ """Base exception."""
+ def __repr__(self):
+ return str(self)
+
+
+class DTDForbidden(DefusedXmlException):
+ """Document type definition is forbidden."""
+ def __init__(self, name, sysid, pubid):
+ self.name = name
+ self.sysid = sysid
+ self.pubid = pubid
+
+ def __str__(self):
+ tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
+ return tpl.format(self.name, self.sysid, self.pubid)
+
+
+class EntitiesForbidden(DefusedXmlException):
+ """Entity definition is forbidden."""
+ def __init__(self, name, value, base, sysid, pubid, notation_name):
+ super(EntitiesForbidden, self).__init__()
+ self.name = name
+ self.value = value
+ self.base = base
+ self.sysid = sysid
+ self.pubid = pubid
+ self.notation_name = notation_name
+
+ def __str__(self):
+ tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
+ return tpl.format(self.name, self.sysid, self.pubid)
+
+
+class ExternalReferenceForbidden(DefusedXmlException):
+ """Resolving an external reference is forbidden."""
+ def __init__(self, context, base, sysid, pubid):
+ super(ExternalReferenceForbidden, self).__init__()
+ self.context = context
+ self.base = base
+ self.sysid = sysid
+ self.pubid = pubid
+
+ def __str__(self):
+ tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
+ return tpl.format(self.sysid, self.pubid)
View
12 packages/Django/django/forms/formsets.py
@@ -16,6 +16,9 @@
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
+# default maximum number of forms in a formset, to prevent memory exhaustion
+DEFAULT_MAX_NUM = 1000
+
class ManagementForm(Form):
"""
``ManagementForm`` is used to keep track of how many form instances
@@ -104,7 +107,7 @@ def initial_form_count(self):
def _construct_forms(self):
# instantiate all the forms and put them in self.forms
self.forms = []
- for i in xrange(self.total_form_count()):
+ for i in xrange(min(self.total_form_count(), self.absolute_max)):
self.forms.append(self._construct_form(i))
def _construct_form(self, i, **kwargs):
@@ -348,9 +351,14 @@ def as_ul(self):
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
can_delete=False, max_num=None):
"""Return a FormSet for the given form class."""
+ if max_num is None:
+ max_num = DEFAULT_MAX_NUM
+ # hard limit on forms instantiated, to prevent memory-exhaustion attacks
+ # limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num
+ absolute_max = max(DEFAULT_MAX_NUM, max_num)
attrs = {'form': form, 'extra': extra,
'can_order': can_order, 'can_delete': can_delete,
- 'max_num': max_num}
+ 'max_num': max_num, 'absolute_max': absolute_max}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):
View
56 packages/Django/django/http/__init__.py
@@ -129,6 +129,8 @@ def __init__(self, *args, **kwargs):
RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
absolute_http_url_re = re.compile(r"^https?://", re.I)
+host_validation_re = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$")
+
class Http404(Exception):
pass
@@ -166,11 +168,15 @@ def get_host(self):
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
+ if settings.DEBUG:
+ allowed_hosts = ['*']
+ else:
+ allowed_hosts = settings.ALLOWED_HOSTS
+ if validate_host(host, allowed_hosts):
+ return host
+ else:
+ raise SuspiciousOperation(
+ "Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host)
def get_full_path(self):
# RFC 3986 requires query string arguments to be in the ASCII range.
@@ -702,3 +708,43 @@ def str_to_unicode(s, encoding):
else:
return s
+def validate_host(host, allowed_hosts):
+ """
+ Validate the given host header value for this site.
+
+ Check that the host looks valid and matches a host or host pattern in the
+ given list of ``allowed_hosts``. Any pattern beginning with a period
+ matches a domain and all its subdomains (e.g. ``.example.com`` matches
+ ``example.com`` and any subdomain), ``*`` matches anything, and anything
+ else must match exactly.
+
+ Return ``True`` for a valid host, ``False`` otherwise.
+
+ """
+ # All validation is case-insensitive
+ host = host.lower()
+
+ # Basic sanity check
+ if not host_validation_re.match(host):
+ return False
+
+ # Validate only the domain part.
+ if host[-1] == ']':
+ # It's an IPv6 address without a port.
+ domain = host
+ else:
+ domain = host.rsplit(':', 1)[0]
+
+ for pattern in allowed_hosts:
+ pattern = pattern.lower()
+ match = (
+ pattern == '*' or
+ pattern.startswith('.') and (
+ domain.endswith(pattern) or domain == pattern[1:]
+ ) or
+ pattern == domain
+ )
+ if match:
+ return True
+
+ return False
View
6 packages/Django/django/test/utils.py
@@ -76,6 +76,9 @@ def setup_test_environment():
mail.original_email_backend = settings.EMAIL_BACKEND
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
+ settings._original_allowed_hosts = settings.ALLOWED_HOSTS
+ settings.ALLOWED_HOSTS = ['*']
+
mail.outbox = []
deactivate()
@@ -97,6 +100,9 @@ def teardown_test_environment():
settings.EMAIL_BACKEND = mail.original_email_backend
del mail.original_email_backend
+ settings.ALLOWED_HOSTS = settings._original_allowed_hosts
+ del settings._original_allowed_hosts
+
del mail.outbox
View
12 packages/Django/django/utils/http.py
@@ -204,3 +204,15 @@ def same_origin(url1, url2):
"""
p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
return p1[0:2] == p2[0:2]
+
+def is_safe_url(url, host=None):
+ """
+ Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
+ a different host).
+
+ Always returns ``False`` on an empty url.
+ """
+ if not url:
+ return False
+ netloc = urlparse.urlparse(url)[1]
+ return not netloc or netloc == host
View
12 packages/Django/django/views/i18n.py
@@ -8,6 +8,8 @@
from django.utils.text import javascript_quote
from django.utils.encoding import smart_unicode
from django.utils.formats import get_format_modules, get_format
+from django.utils.http import is_safe_url
+
def set_language(request):
"""
@@ -20,11 +22,11 @@ def set_language(request):
redirect to the page in the request (the 'next' parameter) without changing
any state.
"""
- next = request.REQUEST.get('next', None)
- if not next:
- next = request.META.get('HTTP_REFERER', None)
- if not next:
- next = '/'
+ next = request.REQUEST.get('next')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = request.META.get('HTTP_REFERER')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = '/'
response = http.HttpResponseRedirect(next)
if request.method == 'POST':
lang_code = request.POST.get('language', None)
View
4 packages/Django/docs/conf.py
@@ -50,9 +50,9 @@
# built documents.
#
# The short X.Y version.
-version = '1.3.4'
+version = '1.3.6'
# The full version, including alpha/beta/rc tags.
-release = '1.3.4'
+release = '1.3.6'
# The next version to be released
django_next_version = '1.4'
View
0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.3.sh 100755 → 100644
File mode changed
View
0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.4.sh 100755 → 100644
File mode changed
View
0  packages/Django/docs/ref/contrib/gis/create_template_postgis-1.5.sh 100755 → 100644
File mode changed
View
0  packages/Django/docs/ref/contrib/gis/create_template_postgis-debian.sh 100755 → 100644
File mode changed
View
36 packages/Django/docs/ref/settings.txt
@@ -82,6 +82,42 @@ of (Full name, e-mail address). Example::
Note that Django will e-mail *all* of these people whenever an error happens.
See :doc:`/howto/error-reporting` for more information.
+.. setting:: ALLOWED_HOSTS
+
+ALLOWED_HOSTS
+-------------
+
+Default: ``['*']``
+
+A list of strings representing the host/domain names that this Django site can
+serve. This is a security measure to prevent an attacker from poisoning caches
+and password reset emails with links to malicious hosts by submitting requests
+with a fake HTTP ``Host`` header, which is possible even under many
+seemingly-safe webserver configurations.
+
+Values in this list can be fully qualified names (e.g. ``'www.example.com'``),
+in which case they will be matched against the request's ``Host`` header
+exactly (case-insensitive, not including port). A value beginning with a period
+can be used as a subdomain wildcard: ``'.example.com'`` will match
+``example.com``, ``www.example.com``, and any other subdomain of
+``example.com``. A value of ``'*'`` will match anything; in this case you are
+responsible to provide your own validation of the ``Host`` header (perhaps in a
+middleware; if so this middleware must be listed first in
+:setting:`MIDDLEWARE_CLASSES`).
+
+If the ``Host`` header (or ``X-Forwarded-Host`` if
+:setting:`USE_X_FORWARDED_HOST` is enabled) does not match any value in this
+list, the :meth:`django.http.HttpRequest.get_host()` method will raise
+:exc:`~django.core.exceptions.SuspiciousOperation`.
+
+When :setting:`DEBUG` is ``True`` or when running tests, host validation is
+disabled; any host will be accepted. Thus it's usually only necessary to set it
+in production.
+
+This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
+if your code accesses the ``Host`` header directly from ``request.META`` you
+are bypassing this security protection.
+
.. setting:: ALLOWED_INCLUDE_ROOTS
ALLOWED_INCLUDE_ROOTS
View
1  packages/Django/docs/releases/index.txt
@@ -19,6 +19,7 @@ Final releases
.. toctree::
:maxdepth: 1
+ 1.3.6
1.3.1
1.3
View
6 packages/Django/docs/topics/forms/formsets.txt
@@ -102,8 +102,10 @@ If the value of ``max_num`` is greater than the number of existing
objects, up to ``extra`` additional blank forms will be added to the formset,
so long as the total number of forms does not exceed ``max_num``.
-A ``max_num`` value of ``None`` (the default) puts no limit on the number of
-forms displayed. Please note that the default value of ``max_num`` was changed
+A ``max_num`` value of ``None`` (the default) puts a high limit on the number
+of forms displayed (1000). In practice this is equivalent to no limit.
+
+Please note that the default value of ``max_num`` was changed
from ``0`` to ``None`` in version 1.2 to allow ``0`` as a valid value.
Formset validation
View
4 packages/Django/docs/topics/forms/modelforms.txt
@@ -691,8 +691,8 @@ so long as the total number of forms does not exceed ``max_num``::
.. versionchanged:: 1.2
-A ``max_num`` value of ``None`` (the default) puts no limit on the number of
-forms displayed.
+A ``max_num`` value of ``None`` (the default) puts a high limit on the number
+of forms displayed (1000). In practice this is equivalent to no limit.
Using a model formset in a view
-------------------------------
View
0  packages/Django/extras/csrf_migration_helper.py 100755 → 100644
File mode changed
View
0  packages/Django/extras/django_bash_completion 100755 → 100644
File mode changed
View
2  packages/Django/setup.py
@@ -77,7 +77,7 @@ def fullsplit(path, result=None):
author = 'Django Software Foundation',
author_email = 'foundation@djangoproject.com',
description = 'A high-level Python Web framework that encourages rapid development and clean, pragmatic design.',
- download_url = 'https://www.djangoproject.com/m/releases/1.3/Django-1.3.4.tar.gz',
+ download_url = 'https://www.djangoproject.com/m/releases/1.3/Django-1.3.6.tar.gz',
packages = packages,
cmdclass = cmdclasses,
data_files = data_files,
View
40 packages/Django/tests/regressiontests/admin_views/tests.py
@@ -827,6 +827,46 @@ def testChangeView(self):
self.assertContains(request, 'login-form')
self.client.get('/test_admin/admin/logout/')
+ def testHistoryView(self):
+ """History view should restrict access."""
+
+ # add user shoud not be able to view the list of article or change any of them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/history/')
+ self.assertEqual(response.status_code, 403)
+ self.client.get('/test_admin/admin/logout/')
+
+ # change user can view all items and edit them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/history/')
+ self.assertEqual(response.status_code, 200)
+
+ # Test redirection when using row-level change permissions. Refs #11513.
+ RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
+ RowLevelChangePermissionModel.objects.create(id=2, name="even id")
+ for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/')
+ self.assertEqual(response.status_code, 403)
+
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/')
+ self.assertEqual(response.status_code, 200)
+
+ self.client.get('/test_admin/admin/logout/')
+
+ for login_dict in [self.joepublic_login, self.no_username_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+
+ self.client.get('/test_admin/admin/logout/')
+
def testConditionallyShowAddSectionLink(self):
"""
The foreign key widget should only show the "add related" button if the
View
0  packages/Django/tests/regressiontests/app_loading/eggs/brokenapp.egg 100755 → 100644
File mode changed
View
0  packages/Django/tests/regressiontests/app_loading/eggs/modelapp.egg 100755 → 100644
File mode changed
View
0  packages/Django/tests/regressiontests/app_loading/eggs/nomodelapp.egg 100755 → 100644
File mode changed
View
0  packages/Django/tests/regressiontests/app_loading/eggs/omelet.egg 100755 → 100644
File mode changed
View
7 packages/Django/tests/regressiontests/comment_tests/tests/comment_view_tests.py
@@ -217,6 +217,13 @@ def testCommentNext(self):
match = re.search(r"^http://testserver/somewhere/else/\?c=\d+$", location)
self.assertTrue(match != None, "Unexpected redirect location: %s" % location)
+ data["next"] = "http://badserver/somewhere/else/"
+ data["comment"] = "This is another comment with an unsafe next url"
+ response = self.client.post("/post/", data)
+ location = response["Location"]
+ match = post_redirect_re.match(location)
+ self.assertTrue(match != None, "Unsafe redirection to: %s" % location)
+
def testCommentDoneView(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
View
89 packages/Django/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
@@ -27,6 +27,30 @@ def testFlagPost(self):
self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1)
return c
+ def testFlagPostNext(self):
+ """
+ POST the flag view, explicitly providing a next url.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/flag/%d/" % pk, {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % pk)
+
+ def testFlagPostUnsafeNext(self):
+ """
+ POSTing to the flag view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/flag/%d/" % pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/flagged/?c=%d" % pk)
+
def testFlagPostTwice(self):
"""Users don't get to flag comments more than once."""
c = self.testFlagPost()
@@ -46,7 +70,7 @@ def testFlagAnon(self):
def testFlaggedView(self):
comments = self.createSomeComments()
pk = comments[0].pk
- response = self.client.get("/flagged/", data={"c":pk})
+ response = self.client.get("/flagged/", data={"c": pk})
self.assertTemplateUsed(response, "comments/flagged.html")
def testFlagSignals(self):
@@ -98,6 +122,33 @@ def testDeletePost(self):
self.assertTrue(c.is_removed)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1)
+ def testDeletePostNext(self):
+ """
+ POSTing the delete view will redirect to an explicitly provided a next
+ url.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/delete/%d/" % pk, {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % pk)
+
+ def testDeletePostUnsafeNext(self):
+ """
+ POSTing to the delete view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/delete/%d/" % pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/deleted/?c=%d" % pk)
+
def testDeleteSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
@@ -113,13 +164,13 @@ def receive(sender, **kwargs):
def testDeletedView(self):
comments = self.createSomeComments()
pk = comments[0].pk
- response = self.client.get("/deleted/", data={"c":pk})
+ response = self.client.get("/deleted/", data={"c": pk})
self.assertTemplateUsed(response, "comments/deleted.html")
class ApproveViewTests(CommentTestCase):
def testApprovePermissions(self):
- """The delete view should only be accessible to 'moderators'"""
+ """The approve view should only be accessible to 'moderators'"""
comments = self.createSomeComments()
pk = comments[0].pk
self.client.login(username="normaluser", password="normaluser")
@@ -131,7 +182,7 @@ def testApprovePermissions(self):
self.assertEqual(response.status_code, 200)
def testApprovePost(self):
- """POSTing the delete view should mark the comment as removed"""
+ """POSTing the approve view should mark the comment as removed"""
c1, c2, c3, c4 = self.createSomeComments()
c1.is_public = False; c1.save()
@@ -143,6 +194,36 @@ def testApprovePost(self):
self.assertTrue(c.is_public)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1)
+ def testApprovePostNext(self):
+ """
+ POSTing the approve view will redirect to an explicitly provided a next
+ url.
+ """
+ c1, c2, c3, c4 = self.createSomeComments()
+ c1.is_public = False; c1.save()
+
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/approve/%d/" % c1.pk,
+ {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % c1.pk)
+
+ def testApprovePostUnsafeNext(self):
+ """
+ POSTing to the approve view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ c1, c2, c3, c4 = self.createSomeComments()
+ c1.is_public = False; c1.save()
+
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/approve/%d/" % c1.pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/approved/?c=%d" % c1.pk)
+
def testApproveSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
View
85 packages/Django/tests/regressiontests/forms/tests/formsets.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from django.forms import Form, CharField, IntegerField, ValidationError, DateField
+from django.forms import Form, CharField, IntegerField, ValidationError, DateField, formsets
from django.forms.formsets import formset_factory, BaseFormSet
from django.utils.unittest import TestCase
@@ -47,7 +47,7 @@ def test_basic_formset(self):
# for adding data. By default, it displays 1 blank form. It can display more,
# but we'll look at how to do so later.
formset = ChoiceFormSet(auto_id=False, prefix='choices')
- self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" />
+ self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""")
@@ -623,8 +623,8 @@ def test_limiting_max_forms(self):
# Limiting the maximum number of forms ########################################
# Base case for max_num.
- # When not passed, max_num will take its default value of None, i.e. unlimited
- # number of forms, only controlled by the value of the extra parameter.
+ # When not passed, max_num will take a high default value, leaving the
+ # number of forms only controlled by the value of the extra parameter.
LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3)
formset = LimitedFavoriteDrinkFormSet()
@@ -671,8 +671,8 @@ def test_limiting_max_forms(self):
def test_max_num_with_initial_data(self):
# max_num with initial data
- # When not passed, max_num will take its default value of None, i.e. unlimited
- # number of forms, only controlled by the values of the initial and extra
+ # When not passed, max_num will take a high default value, leaving the
+ # number of forms only controlled by the value of the initial and extra
# parameters.
initial = [
@@ -805,6 +805,65 @@ def __iter__(self):
self.assertEqual(str(reverse_formset[1]), str(forms[-2]))
self.assertEqual(len(reverse_formset), len(forms))
+ def test_hard_limit_on_instantiated_forms(self):
+ """A formset has a hard limit on the number of forms instantiated."""
+ # reduce the default limit of 1000 temporarily for testing
+ _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
+ try:
+ formsets.DEFAULT_MAX_NUM = 3
+ ChoiceFormSet = formset_factory(Choice)
+ # someone fiddles with the mgmt form data...
+ formset = ChoiceFormSet(
+ {
+ 'choices-TOTAL_FORMS': '4',
+ 'choices-INITIAL_FORMS': '0',
+ 'choices-MAX_NUM_FORMS': '4',
+ 'choices-0-choice': 'Zero',
+ 'choices-0-votes': '0',
+ 'choices-1-choice': 'One',
+ 'choices-1-votes': '1',
+ 'choices-2-choice': 'Two',
+ 'choices-2-votes': '2',
+ 'choices-3-choice': 'Three',
+ 'choices-3-votes': '3',
+ },
+ prefix='choices',
+ )
+ # But we still only instantiate 3 forms
+ self.assertEqual(len(formset.forms), 3)
+ finally:
+ formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
+
+ def test_increase_hard_limit(self):
+ """Can increase the built-in forms limit via a higher max_num."""
+ # reduce the default limit of 1000 temporarily for testing
+ _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
+ try:
+ formsets.DEFAULT_MAX_NUM = 3
+ # for this form, we want a limit of 4
+ ChoiceFormSet = formset_factory(Choice, max_num=4)
+ formset = ChoiceFormSet(
+ {
+ 'choices-TOTAL_FORMS': '4',
+ 'choices-INITIAL_FORMS': '0',
+ 'choices-MAX_NUM_FORMS': '4',
+ 'choices-0-choice': 'Zero',
+ 'choices-0-votes': '0',
+ 'choices-1-choice': 'One',
+ 'choices-1-votes': '1',
+ 'choices-2-choice': 'Two',
+ 'choices-2-votes': '2',
+ 'choices-3-choice': 'Three',
+ 'choices-3-votes': '3',
+ },
+ prefix='choices',
+ )
+ # This time four forms are instantiated
+ self.assertEqual(len(formset.forms), 4)
+ finally:
+ formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
+
+
data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data
@@ -900,12 +959,12 @@ def test_empty_forms_are_unbound(self):
# The empty forms should be equal.
self.assertEqual(empty_forms[0].as_p(), empty_forms[1].as_p())
-class TestEmptyFormSet(TestCase):
+class TestEmptyFormSet(TestCase):
"Test that an empty formset still calls clean()"
- def test_empty_formset_is_valid(self):
- EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
- formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form")
- formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form")
- self.assertFalse(formset.is_valid())
- self.assertFalse(formset2.is_valid())
+ def test_empty_formset_is_valid(self):
+ EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
+ formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form")
+ formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form")
+ self.assertFalse(formset.is_valid())
+ self.assertFalse(formset2.is_valid())
View
153 packages/Django/tests/regressiontests/requests/tests.py
@@ -1,9 +1,11 @@
+# -*- coding: utf-8 -*-
import time
from datetime import datetime, timedelta
from StringIO import StringIO
from django.conf import settings
from django.core.handlers.modpython import ModPythonRequest
+from django.core.exceptions import SuspiciousOperation
from django.core.handlers.wsgi import WSGIRequest, LimitedStream
from django.http import HttpRequest, HttpResponse, parse_cookie
from django.utils import unittest
@@ -61,17 +63,23 @@ def test_httprequest_location(self):
'http://www.example.com/path/with:colons')
def test_http_get_host(self):
- old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+ _old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+ _old_ALLOWED_HOSTS = settings.ALLOWED_HOSTS
try:
settings.USE_X_FORWARDED_HOST = False
+ settings.ALLOWED_HOSTS = [
+ 'forward.com', 'example.com', 'internal.com', '12.34.56.78',
+ '[2001:19f0:feee::dead:beef:cafe]', 'xn--4ca9at.com',
+ '.multitenant.com', 'INSENSITIVE.com',
+ ]
# Check if X_FORWARDED_HOST is provided.
request = HttpRequest()
request.META = {
- u'HTTP_X_FORWARDED_HOST': u'forward.com',
- u'HTTP_HOST': u'example.com',
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'HTTP_X_FORWARDED_HOST': 'forward.com',
+ 'HTTP_HOST': 'example.com',
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
# X_FORWARDED_HOST is ignored.
self.assertEqual(request.get_host(), 'example.com')
@@ -79,43 +87,84 @@ def test_http_get_host(self):
# Check if X_FORWARDED_HOST isn't provided.
request = HttpRequest()
request.META = {
- u'HTTP_HOST': u'example.com',
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'HTTP_HOST': 'example.com',
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
self.assertEqual(request.get_host(), 'example.com')
# Check if HTTP_HOST isn't provided.
request = HttpRequest()
request.META = {
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
self.assertEqual(request.get_host(), 'internal.com')
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
request = HttpRequest()
request.META = {
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 8042,
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 8042,
}
self.assertEqual(request.get_host(), 'internal.com:8042')
+ # Poisoned host headers are rejected as suspicious
+ legit_hosts = [
+ 'example.com',
+ 'example.com:80',
+ '12.34.56.78',
+ '12.34.56.78:443',
+ '[2001:19f0:feee::dead:beef:cafe]',
+ '[2001:19f0:feee::dead:beef:cafe]:8080',
+ 'xn--4ca9at.com', # Punnycode for öäü.com
+ 'anything.multitenant.com',
+ 'multitenant.com',
+ 'insensitive.com',
+ ]
+
+ poisoned_hosts = [
+ 'example.com@evil.tld',
+ 'example.com:dr.frankenstein@evil.tld',
+ 'example.com:dr.frankenstein@evil.tld:80',
+ 'example.com:80/badpath',
+ 'example.com: recovermypassword.com',
+ 'other.com', # not in ALLOWED_HOSTS
+ ]
+
+ for host in legit_hosts:
+ request = HttpRequest()
+ request.META = {
+ 'HTTP_HOST': host,
+ }
+ request.get_host()
+
+ for host in poisoned_hosts:
+ def _test():
+ request = HttpRequest()
+ request.META = {
+ 'HTTP_HOST': host,
+ }
+ request.get_host()
+ self.assertRaises(SuspiciousOperation, _test)
finally:
- settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
+ settings.ALLOWED_HOSTS = _old_ALLOWED_HOSTS
+ settings.USE_X_FORWARDED_HOST = _old_USE_X_FORWARDED_HOST
def test_http_get_host_with_x_forwarded_host(self):
- old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+ _old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+ _old_ALLOWED_HOSTS = settings.ALLOWED_HOSTS
try:
settings.USE_X_FORWARDED_HOST = True
+ settings.ALLOWED_HOSTS = ['*']
# Check if X_FORWARDED_HOST is provided.
request = HttpRequest()
request.META = {
- u'HTTP_X_FORWARDED_HOST': u'forward.com',
- u'HTTP_HOST': u'example.com',
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'HTTP_X_FORWARDED_HOST': 'forward.com',
+ 'HTTP_HOST': 'example.com',
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
# X_FORWARDED_HOST is obeyed.
self.assertEqual(request.get_host(), 'forward.com')
@@ -123,30 +172,82 @@ def test_http_get_host_with_x_forwarded_host(self):
# Check if X_FORWARDED_HOST isn't provided.
request = HttpRequest()
request.META = {
- u'HTTP_HOST': u'example.com',
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'HTTP_HOST': 'example.com',
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
self.assertEqual(request.get_host(), 'example.com')
# Check if HTTP_HOST isn't provided.
request = HttpRequest()
request.META = {
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 80,
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 80,
}
self.assertEqual(request.get_host(), 'internal.com')
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
request = HttpRequest()
request.META = {
- u'SERVER_NAME': u'internal.com',
- u'SERVER_PORT': 8042,
+ 'SERVER_NAME': 'internal.com',
+ 'SERVER_PORT': 8042,
}
self.assertEqual(request.get_host(), 'internal.com:8042')
+ # Poisoned host headers are rejected as suspicious
+ legit_hosts = [
+ 'example.com',
+ 'example.com:80',
+ '12.34.56.78',
+ '12.34.56.78:443',
+ '[2001:19f0:feee::dead:beef:cafe]',
+ '[2001:19f0:feee::dead:beef:cafe]:8080',
+ 'xn--4ca9at.com', # Punnycode for öäü.com
+ ]
+
+ poisoned_hosts = [
+ 'example.com@evil.tld',
+ 'example.com:dr.frankenstein@evil.tld',
+ 'example.com:dr.frankenstein@evil.tld:80',
+ 'example.com:80/badpath',
+ 'example.com: recovermypassword.com',
+ ]
+
+ for host in legit_hosts:
+ request = HttpRequest()
+ request.META = {
+ 'HTTP_HOST': host,
+ }
+ request.get_host()
+
+ for host in poisoned_hosts:
+ def _test():
+ request = HttpRequest()
+ request.META = {
+ 'HTTP_HOST': host,
+ }
+ request.get_host()
+ self.assertRaises(SuspiciousOperation, _test)
finally:
- settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
+ settings.ALLOWED_HOSTS = _old_ALLOWED_HOSTS
+ settings.USE_X_FORWARDED_HOST = _old_USE_X_FORWARDED_HOST
+
+ def test_host_validation_disabled_in_debug_mode(self):
+ """If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass."""
+ _old_DEBUG = settings.DEBUG
+ _old_ALLOWED_HOSTS = settings.ALLOWED_HOSTS
+ try:
+ settings.DEBUG = True
+ settings.ALLOWED_HOSTS = []
+
+ request = HttpRequest()
+ request.META = {
+ 'HTTP_HOST': 'example.com',
+ }
+ self.assertEqual(request.get_host(), 'example.com')
+ finally:
+ settings.DEBUG = _old_DEBUG
+ settings.ALLOWED_HOSTS = _old_ALLOWED_HOSTS
def test_near_expiration(self):
"Cookie will expire when an near expiration time is provided"
View
15 packages/Django/tests/regressiontests/serializers_regress/tests.py
@@ -14,6 +14,7 @@
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
+from django.core.serializers.xml_serializer import DTDForbidden
from django.conf import settings
from django.core import serializers, management
@@ -416,3 +417,17 @@ def streamTest(format, self):
setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))
if format != 'python':
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
+
+
+class XmlDeserializerSecurityTests(TestCase):
+
+ def test_no_dtd(self):
+ """
+ The XML deserializer shouldn't allow a DTD.
+
+ This is the most straightforward way to prevent all entity definitions
+ and avoid both external entities and entity-expansion attacks.
+
+ """
+ xml = '<?xml version="1.0" standalone="no"?><!DOCTYPE example SYSTEM "http://example.com/example.dtd">'
+ self.assertRaises(DTDForbidden, serializers.deserialize('xml', xml).next)
View
0  packages/Django/tests/regressiontests/templates/eggs/tagsegg.egg 100755 → 100644
File mode changed
View
0  packages/Django/tests/regressiontests/templates/templates/test_extends_error.html 100755 → 100644
File mode changed
View
17 packages/Django/tests/regressiontests/views/tests/i18n.py
@@ -13,13 +13,28 @@ class I18NTests(TestCase):
""" Tests django views in django/views/i18n.py """
def test_setlang(self):
- """The set_language view can be used to change the session language"""
+ """
+ The set_language view can be used to change the session language.
+
+ The user is redirected to the 'next' argument if provided.
+ """
for lang_code, lang_name in settings.LANGUAGES:
post_data = dict(language=lang_code, next='/views/')
response = self.client.post('/views/i18n/setlang/', data=post_data)
self.assertRedirects(response, 'http://testserver/views/')
self.assertEqual(self.client.session['django_language'], lang_code)
+ def test_setlang_unsafe_next(self):
+ """
+ The set_language view only redirects to the 'next' argument if it is
+ "safe".
+ """
+ lang_code, lang_name = settings.LANGUAGES[0]
+ post_data = dict(language=lang_code, next='//unsafe/redirection/')
+ response = self.client.post('/views/i18n/setlang/', data=post_data)
+ self.assertEqual(response['Location'], 'http://testserver/')
+ self.assertEqual(self.client.session['django_language'], lang_code)
+
def test_jsi18n(self):
"""The javascript_catalog can be deployed with language settings"""
for lang_code in ['es', 'fr', 'ru']:
View
0  packages/Django/tests/runtests.py 100755 → 100644
File mode changed
View
79 packages/django/docs/releases/1.3.6.txt
@@ -0,0 +1,79 @@
+==========================
+Django 1.3.6 release notes
+==========================
+
+*February 19, 2013*
+
+Django 1.3.6 fixes four security issues present in previous Django releases in
+the 1.3 series.
+
+This is the sixth bugfix/security release in the Django 1.3 series.
+
+
+Host header poisoning
+---------------------
+
+Some parts of Django -- independent of end-user-written applications -- make
+use of full URLs, including domain name, which are generated from the HTTP Host
+header. Django's documentation has for some time contained notes advising users
+on how to configure webservers to ensure that only valid Host headers can reach
+the Django application. However, it has been reported to us that even with the
+recommended webserver configurations there are still techniques available for
+tricking many common webservers into supplying the application with an
+incorrect and possibly malicious Host header.
+
+For this reason, Django 1.3.6 adds a new setting, ``ALLOWED_HOSTS``, which
+should contain an explicit list of valid host/domain names for this site. A
+request with a Host header not matching an entry in this list will raise
+``SuspiciousOperation`` if ``request.get_host()`` is called. For full details
+see the documentation for the :setting:`ALLOWED_HOSTS` setting.
+
+The default value for this setting in Django 1.3.6 is ``['*']`` (matching any
+host), for backwards-compatibility, but we strongly encourage all sites to set
+a more restrictive value.
+
+This host validation is disabled when ``DEBUG`` is ``True`` or when running tests.
+
+
+XML deserialization
+-------------------
+
+The XML parser in the Python standard library is vulnerable to a number of
+denial-of-service attacks via external entities and entity expansion. Django
+uses this parser for deserializing XML-formatted database fixtures. The fixture
+deserializer is not intended for use with untrusted data, but in order to err
+on the side of safety in Django 1.3.6 the XML deserializer refuses to parse an
+XML document with a DTD (DOCTYPE definition), which closes off these attack
+avenues.
+
+These issues in the Python standard library are CVE-2013-1664 and
+CVE-2013-1665. More information available `from the Python security team`_.
+
+Django's XML serializer does not create documents with a DTD, so this should
+not cause any issues with the typical round-trip from ``dumpdata`` to
+``loaddata``, but if you feed your own XML documents to the ``loaddata``
+management command, you will need to ensure they do not contain a DTD.
+
+.. _from the Python security team: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html
+
+
+Formset memory exhaustion
+-------------------------
+
+Previous versions of Django did not validate or limit the form-count data
+provided by the client in a formset's management form, making it possible to
+exhaust a server's available memory by forcing it to create very large numbers
+of forms.
+
+In Django 1.3.6, all formsets have a strictly-enforced maximum number of forms
+(1000 by default, though it can be set higher via the ``max_num`` formset
+factory argument).
+
+
+Admin history view information leakage
+--------------------------------------
+
+In previous versions of Django, an admin user without change permission on a
+model could still view the unicode representation of instances via their admin
+history log. Django 1.3.6 now limits the admin history log view for an object
+to users with change permission for that model.
Please sign in to comment.
Something went wrong with that request. Please try again.