Compare commits

...

13 Commits

Author SHA1 Message Date
James Bennett c939b2a1cb [0.96-bugfixes] Update download_url.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11472 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-08-19 23:54:28 +00:00
James Bennett c4c00358ee [0.96-bugfixes] Bump version information for 0.96.5 bugfix release.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11469 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-08-19 23:42:22 +00:00
Russell Keith-Magee c60c89a76e [0.96.X] Another update to security patch from r11354.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11435 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-08-12 08:32:49 +00:00
Russell Keith-Magee 9cc89c97b9 Added file accidentally ommitted from [11354].
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11430 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-08-11 13:06:32 +00:00
James Bennett 40598612d1 [0.96-bugfixes] Update packaging information for impending 0.96.4 security/bugfix release.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11356 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-07-29 03:30:13 +00:00
Russell Keith-Magee da85d76fd6 [0.96.X] SECURITY ALERT: Corrected a problem with the Admin media handler that could lead to the exposure of system files. Thanks to Gary Wilson for the patch.
This is a security-related backport of r11351. A full announcement will be forthcoming.


git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@11354 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-07-29 02:57:43 +00:00
James Bennett 1232a46765 Bump legacy support branch to 0.96.3 for impending security release
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@8880 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-09-02 21:19:42 +00:00
Jacob Kaplan-Moss 7e0972bded Security fix. Announcement forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@8877 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-09-02 21:10:00 +00:00
James Bennett 3ca5a055a7 Version bump 0.96.1 -> 0.96.2 for forthcoming security bugfix release
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@7530 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-05-14 04:15:22 +00:00
James Bennett 7791e5c050 Backport [7521] to 0.96-bugfixes per security policy; announcement and security bugfix release will be forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@7527 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2008-05-14 04:06:46 +00:00
Jacob Kaplan-Moss 7dd2dd08a7 i18n security fix. Details will be posted shortly to the Django mailing lists and the official weblog.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@6607 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-26 19:52:29 +00:00
Jacob Kaplan-Moss 6c1c7c9e4f Created 0.96-bugfixes branch.
git-svn-id: http://code.djangoproject.com/svn/django/branches/0.96-bugfixes@6604 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-10-26 18:43:33 +00:00
Jacob Kaplan-Moss c48165765e Re-tagged the 0.96 release.
git-svn-id: http://code.djangoproject.com/svn/django/tags/releases/0.96@4810 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-03-23 21:02:44 +00:00
13 changed files with 219 additions and 102 deletions

View File

@ -1 +1 @@
VERSION = (0, 96, None) VERSION = (0, 96.5, None)

View File

@ -237,7 +237,7 @@ TRANSACTIONS_MANAGED = False
# The User-Agent string to use when checking for URL validity through the # The User-Agent string to use when checking for URL validity through the
# isExistingURL validator. # isExistingURL validator.
URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)" URL_VALIDATOR_USER_AGENT = "Django/0.96.5 (http://www.djangoproject.com)"
############## ##############
# MIDDLEWARE # # MIDDLEWARE #

View File

@ -19,7 +19,6 @@
<div class="form-row"> <div class="form-row">
<label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" /> <label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" />
<input type="hidden" name="this_is_the_login_form" value="1" /> <input type="hidden" name="this_is_the_login_form" value="1" />
<input type="hidden" name="post_data" value="{{ post_data }}" /> {#<span class="help">{% trans 'Have you <a href="/password_reset/">forgotten your password</a>?' %}</span>#}
</div> </div>
<div class="submit-row"> <div class="submit-row">
<label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" /> <label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" />

View File

@ -3,43 +3,21 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.utils.html import escape
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
import base64, datetime, md5 import base64, datetime
import cPickle as pickle
ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
LOGIN_FORM_KEY = 'this_is_the_login_form' LOGIN_FORM_KEY = 'this_is_the_login_form'
def _display_login_form(request, error_message=''): def _display_login_form(request, error_message=''):
request.session.set_test_cookie() request.session.set_test_cookie()
if request.POST and request.POST.has_key('post_data'):
# User has failed login BUT has previously saved post data.
post_data = request.POST['post_data']
elif request.POST:
# User's session must have expired; save their post data.
post_data = _encode_post_data(request.POST)
else:
post_data = _encode_post_data({})
return render_to_response('admin/login.html', { return render_to_response('admin/login.html', {
'title': _('Log in'), 'title': _('Log in'),
'app_path': request.path, 'app_path': escape(request.path),
'post_data': post_data,
'error_message': error_message 'error_message': error_message
}, context_instance=template.RequestContext(request)) }, context_instance=template.RequestContext(request))
def _encode_post_data(post_data):
pickled = pickle.dumps(post_data)
pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
return base64.encodestring(pickled + pickled_md5)
def _decode_post_data(encoded_data):
encoded_data = base64.decodestring(encoded_data)
pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
from django.core.exceptions import SuspiciousOperation
raise SuspiciousOperation, "User may have tampered with session cookie."
return pickle.loads(pickled)
def staff_member_required(view_func): def staff_member_required(view_func):
""" """
Decorator for views that checks that the user is logged in and is a staff Decorator for views that checks that the user is logged in and is a staff
@ -48,10 +26,6 @@ def staff_member_required(view_func):
def _checklogin(request, *args, **kwargs): def _checklogin(request, *args, **kwargs):
if request.user.is_authenticated() and request.user.is_staff: if request.user.is_authenticated() and request.user.is_staff:
# The user is valid. Continue to the admin page. # The user is valid. Continue to the admin page.
if request.POST.has_key('post_data'):
# User must have re-authenticated through a different window
# or tab.
request.POST = _decode_post_data(request.POST['post_data'])
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
@ -59,7 +33,7 @@ def staff_member_required(view_func):
# If this isn't already the login page, display it. # If this isn't already the login page, display it.
if not request.POST.has_key(LOGIN_FORM_KEY): if not request.POST.has_key(LOGIN_FORM_KEY):
if request.POST: if request.POST:
message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.") message = _("Please log in again, because your session has expired.")
else: else:
message = "" message = ""
return _display_login_form(request, message) return _display_login_form(request, message)
@ -92,16 +66,7 @@ def staff_member_required(view_func):
# TODO: set last_login with an event. # TODO: set last_login with an event.
user.last_login = datetime.datetime.now() user.last_login = datetime.datetime.now()
user.save() user.save()
if request.POST.has_key('post_data'): return http.HttpResponseRedirect(request.path)
post_data = _decode_post_data(request.POST['post_data'])
if post_data and not post_data.has_key(LOGIN_FORM_KEY):
# overwrite request.POST with the saved post_data, and continue
request.POST = post_data
request.user = user
return view_func(request, *args, **kwargs)
else:
request.session.delete_test_cookie()
return http.HttpResponseRedirect(request.path)
else: else:
return _display_login_form(request, ERROR_MESSAGE) return _display_login_form(request, ERROR_MESSAGE)

View File

@ -1192,9 +1192,7 @@ def runserver(addr, port, use_reloader=True, admin_media_dir=''):
print "Development server is running at http://%s:%s/" % (addr, port) print "Development server is running at http://%s:%s/" % (addr, port)
print "Quit the server with %s." % quit_command print "Quit the server with %s." % quit_command
try: try:
import django handler = AdminMediaHandler(WSGIHandler(), admin_media_dir)
path = admin_media_dir or django.__path__[0] + '/contrib/admin/media'
handler = AdminMediaHandler(WSGIHandler(), path)
run(addr, int(port), handler) run(addr, int(port), handler)
except WSGIServerException, e: except WSGIServerException, e:
# Use helpful error messages instead of ugly tracebacks. # Use helpful error messages instead of ugly tracebacks.

View File

@ -11,6 +11,8 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from types import ListType, StringType from types import ListType, StringType
import os, re, sys, time, urllib import os, re, sys, time, urllib
from django.utils._os import safe_join
__version__ = "0.1" __version__ = "0.1"
__all__ = ['WSGIServer','WSGIRequestHandler','demo_app'] __all__ = ['WSGIServer','WSGIRequestHandler','demo_app']
@ -599,11 +601,25 @@ class AdminMediaHandler(object):
self.application = application self.application = application
if not media_dir: if not media_dir:
import django import django
self.media_dir = django.__path__[0] + '/contrib/admin/media' self.media_dir = \
os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
else: else:
self.media_dir = media_dir self.media_dir = media_dir
self.media_url = settings.ADMIN_MEDIA_PREFIX self.media_url = settings.ADMIN_MEDIA_PREFIX
def file_path(self, url):
"""
Returns the path to the media file on disk for the given URL.
The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX. If the
resultant file path is outside the media directory, then a ValueError
is raised.
"""
# Remove ADMIN_MEDIA_PREFIX.
relative_url = url[len(self.media_url):]
relative_path = urllib.url2pathname(relative_url)
return safe_join(self.media_dir, relative_path)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
import os.path import os.path
@ -614,19 +630,25 @@ class AdminMediaHandler(object):
return self.application(environ, start_response) return self.application(environ, start_response)
# Find the admin file and serve it up, if it exists and is readable. # Find the admin file and serve it up, if it exists and is readable.
relative_url = environ['PATH_INFO'][len(self.media_url):] try:
file_path = os.path.join(self.media_dir, relative_url) file_path = self.file_path(environ['PATH_INFO'])
except ValueError: # Resulting file path was not valid.
status = '404 NOT FOUND'
headers = {'Content-type': 'text/plain'}
output = ['Page not found: %s' % environ['PATH_INFO']]
start_response(status, headers.items())
return output
if not os.path.exists(file_path): if not os.path.exists(file_path):
status = '404 NOT FOUND' status = '404 NOT FOUND'
headers = {'Content-type': 'text/plain'} headers = {'Content-type': 'text/plain'}
output = ['Page not found: %s' % file_path] output = ['Page not found: %s' % environ['PATH_INFO']]
else: else:
try: try:
fp = open(file_path, 'rb') fp = open(file_path, 'rb')
except IOError: except IOError:
status = '401 UNAUTHORIZED' status = '401 UNAUTHORIZED'
headers = {'Content-type': 'text/plain'} headers = {'Content-type': 'text/plain'}
output = ['Permission denied: %s' % file_path] output = ['Permission denied: %s' % environ['PATH_INFO']]
else: else:
status = '200 OK' status = '200 OK'
headers = {} headers = {}

28
django/utils/_os.py Normal file
View File

@ -0,0 +1,28 @@
"""
A back-ported version of the same module from the 1.0.x branch, without the
unicode support.
"""
from os.path import join, normcase, abspath, sep
def safe_join(base, *paths):
"""
Joins one or more path components to the base path component intelligently.
Returns a normalized, absolute version of the final path.
The final path must be located inside of the base path component (otherwise
a ValueError is raised).
"""
# We need to use normcase to ensure we don't false-negative on case
# insensitive operating systems (like Windows).
final_path = normcase(abspath(join(base, *paths)))
base_path = normcase(abspath(base))
base_path_len = len(base_path)
# Ensure final_path starts with base_path and that the next character after
# the final path is os.sep (or nothing, in which case final_path must be
# equal to base_path).
if not final_path.startswith(base_path) \
or final_path[base_path_len:base_path_len+1] not in ('', sep):
raise ValueError('the joined path is located outside of the base path'
' component')
return final_path

View File

@ -1,6 +1,9 @@
"Translation helper functions" "Translation helper functions"
import os, re, sys import locale
import os
import re
import sys
import gettext as gettext_module import gettext as gettext_module
from cStringIO import StringIO from cStringIO import StringIO
from django.utils.functional import lazy from django.utils.functional import lazy
@ -25,15 +28,25 @@ _active = {}
# The default translation is based on the settings file. # The default translation is based on the settings file.
_default = None _default = None
# This is a cache for accept-header to translation object mappings to prevent # This is a cache for normalised accept-header languages to prevent multiple
# the accept parser to run multiple times for one user. # file lookups when checking the same locale on repeated requests.
_accepted = {} _accepted = {}
def to_locale(language): # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
accept_language_re = re.compile(r'''
([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
(?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
(?:\s*,\s*|$) # Multiple accepts per header.
''', re.VERBOSE)
def to_locale(language, to_lower=False):
"Turns a language name (en-us) into a locale name (en_US)." "Turns a language name (en-us) into a locale name (en_US)."
p = language.find('-') p = language.find('-')
if p >= 0: if p >= 0:
return language[:p].lower()+'_'+language[p+1:].upper() if to_lower:
return language[:p].lower()+'_'+language[p+1:].lower()
else:
return language[:p].lower()+'_'+language[p+1:].upper()
else: else:
return language.lower() return language.lower()
@ -309,46 +322,40 @@ def get_language_from_request(request):
if lang_code in supported and lang_code is not None and check_for_language(lang_code): if lang_code in supported and lang_code is not None and check_for_language(lang_code):
return lang_code return lang_code
lang_code = request.COOKIES.get('django_language', None) lang_code = request.COOKIES.get('django_language')
if lang_code in supported and lang_code is not None and check_for_language(lang_code): if lang_code and lang_code in supported and check_for_language(lang_code):
return lang_code return lang_code
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None) accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
if accept is not None: for lang, unused in parse_accept_lang_header(accept):
if lang == '*':
break
t = _accepted.get(accept, None) # We have a very restricted form for our language files (no encoding
if t is not None: # specifier, since they all must be UTF-8 and only one possible
return t # language each time. So we avoid the overhead of gettext.find() and
# look up the MO file manually.
def _parsed(el): normalized = locale.locale_alias.get(to_locale(lang, True))
p = el.find(';q=') if not normalized:
if p >= 0: continue
lang = el[:p].strip()
order = int(float(el[p+3:].strip())*100)
else:
lang = el
order = 100
p = lang.find('-')
if p >= 0:
mainlang = lang[:p]
else:
mainlang = lang
return (lang, mainlang, order)
langs = [_parsed(el) for el in accept.split(',')] # Remove the default encoding from locale_alias
langs.sort(lambda a,b: -1*cmp(a[2], b[2])) normalized = normalized.split('.')[0]
for lang, mainlang, order in langs: if normalized in _accepted:
if lang in supported or mainlang in supported: # We've seen this locale before and have an MO file for it, so no
langfile = gettext_module.find('django', globalpath, [to_locale(lang)]) # need to check again.
if langfile: return _accepted[normalized]
# reconstruct the actual language from the language
# filename, because otherwise we might incorrectly for lang in (normalized, normalized.split('_')[0]):
# report de_DE if we only have de available, but if lang not in supported:
# did find de_DE because of language normalization continue
lang = langfile[len(globalpath):].split(os.path.sep)[1] langfile = os.path.join(globalpath, lang, 'LC_MESSAGES',
_accepted[accept] = lang 'django.mo')
return lang if os.path.exists(langfile):
_accepted[normalized] = lang
return lang
return settings.LANGUAGE_CODE return settings.LANGUAGE_CODE
@ -494,3 +501,24 @@ def string_concat(*strings):
return ''.join([str(el) for el in strings]) return ''.join([str(el) for el in strings])
string_concat = lazy(string_concat, str) string_concat = lazy(string_concat, str)
def parse_accept_lang_header(lang_string):
"""
Parses the lang_string, which is the body of an HTTP Accept-Language
header, and returns a list of (lang, q-value), ordered by 'q' values.
Any format errors in lang_string results in an empty list being returned.
"""
result = []
pieces = accept_language_re.split(lang_string)
if pieces[-1]:
return []
for i in range(0, len(pieces) - 1, 3):
first, lang, priority = pieces[i : i + 3]
if first:
return []
priority = priority and float(priority) or 1.0
result.append((lang, priority))
result.sort(lambda x, y: -cmp(x[1], y[1]))
return result

View File

@ -1,12 +1,12 @@
================================= ===================================
Django version 0.96 release notes Django version 0.96.1 release notes
================================= ===================================
Welcome to Django 0.96! Welcome to Django 0.96.1!
The primary goal for 0.96 is a cleanup and stabilization of the features The primary goal for 0.96 is a cleanup and stabilization of the features
introduced in 0.95. There have been a few small `backwards-incompatible introduced in 0.95. There have been a few small `backwards-incompatible
changes`_ since 0.95, but the upgrade process should be fairly simple changes since 0.95`_, but the upgrade process should be fairly simple
and should not require major changes to existing applications. and should not require major changes to existing applications.
However, we're also releasing 0.96 now because we have a set of However, we're also releasing 0.96 now because we have a set of
@ -17,9 +17,21 @@ next official release; then you'll be able to upgrade in one step
instead of needing to make incremental changes to keep up with the instead of needing to make incremental changes to keep up with the
development version of Django. development version of Django.
Backwards-incompatible changes Changes since the 0.96 release
============================== ==============================
This release contains fixes for a security vulnerability discovered after the
initial release of Django 0.96. A bug in the i18n framework could allow an
attacker to send extremely large strings in the Accept-Language header and
cause a denial of service by filling available memory.
Because this problems wasn't discovered and fixed until after the 0.96
release, it's recommended that you use this release rather than the original
0.96.
Backwards-incompatible changes since 0.95
=========================================
The following changes may require you to update your code when you switch from The following changes may require you to update your code when you switch from
0.95 to 0.96: 0.95 to 0.96:

View File

@ -32,15 +32,13 @@ if len(sys.argv) > 1 and sys.argv[1] == 'bdist_wininst':
for file_info in data_files: for file_info in data_files:
file_info[0] = '/PURELIB/%s' % file_info[0] file_info[0] = '/PURELIB/%s' % file_info[0]
# Dynamically calculate the version based on django.VERSION.
version = "%d.%d-%s" % (__import__('django').VERSION)
setup( setup(
name = "Django", name = "Django",
version = version, version = "0.96.5",
url = 'http://www.djangoproject.com/', url = 'http://www.djangoproject.com/',
author = 'Lawrence Journal-World', author = 'Django Software Foundation',
author_email = 'holovaty@gmail.com', author_email = 'foundation@djangoproject.com',
download_url = 'http://media.djangoproject.com/releases/0.96/Django-0.96.5.tar.gz',
description = 'A high-level Python Web framework that encourages rapid development and clean, pragmatic design.', description = 'A high-level Python Web framework that encourages rapid development and clean, pragmatic design.',
packages = packages, packages = packages,
data_files = data_files, data_files = data_files,

View File

View File

@ -0,0 +1,67 @@
"""
Tests for django.core.servers.
"""
import os
import django
from django.test import TestCase
from django.core.handlers.wsgi import WSGIHandler
from django.core.servers.basehttp import AdminMediaHandler
class AdminMediaHandlerTests(TestCase):
def setUp(self):
self.admin_media_file_path = \
os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
self.handler = AdminMediaHandler(WSGIHandler())
def test_media_urls(self):
"""
Tests that URLs that look like absolute file paths after the
settings.ADMIN_MEDIA_PREFIX don't turn into absolute file paths.
"""
# Cases that should work on all platforms.
data = (
('/media/css/base.css', ('css', 'base.css')),
)
# Cases that should raise an exception.
bad_data = ()
# Add platform-specific cases.
if os.sep == '/':
data += (
# URL, tuple of relative path parts.
('/media/\\css/base.css', ('\\css', 'base.css')),
)
bad_data += (
'/media//css/base.css',
'/media////css/base.css',
'/media/../css/base.css',
)
elif os.sep == '\\':
bad_data += (
'/media/C:\css/base.css',
'/media//\\css/base.css',
'/media/\\css/base.css',
'/media/\\\\css/base.css'
)
for url, path_tuple in data:
try:
output = self.handler.file_path(url)
except ValueError:
self.fail("Got a ValueError exception, but wasn't expecting"
" one. URL was: %s" % url)
rel_path = os.path.join(*path_tuple)
desired = os.path.normcase(
os.path.join(self.admin_media_file_path, rel_path))
self.assertEqual(output, desired,
"Got: %s, Expected: %s, URL was: %s" % (output, desired, url))
for url in bad_data:
try:
output = self.handler.file_path(url)
except ValueError:
continue
self.fail('URL: %s should have caused a ValueError exception.'
% url)