Skip to content

Commit 7dd2dd0

Browse files
committed
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
1 parent 6c1c7c9 commit 7dd2dd0

File tree

5 files changed

+92
-55
lines changed

5 files changed

+92
-55
lines changed

django/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = (0, 96, None)
1+
VERSION = (0, 96.1, None)

django/conf/global_settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@
237237

238238
# The User-Agent string to use when checking for URL validity through the
239239
# isExistingURL validator.
240-
URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)"
240+
URL_VALIDATOR_USER_AGENT = "Django/0.96.1 (http://www.djangoproject.com)"
241241

242242
##############
243243
# MIDDLEWARE #

django/utils/translation/trans_real.py

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"Translation helper functions"
22

3-
import os, re, sys
3+
import locale
4+
import os
5+
import re
6+
import sys
47
import gettext as gettext_module
58
from cStringIO import StringIO
69
from django.utils.functional import lazy
@@ -25,15 +28,25 @@ def currentThread():
2528
# The default translation is based on the settings file.
2629
_default = None
2730

28-
# This is a cache for accept-header to translation object mappings to prevent
29-
# the accept parser to run multiple times for one user.
31+
# This is a cache for normalised accept-header languages to prevent multiple
32+
# file lookups when checking the same locale on repeated requests.
3033
_accepted = {}
3134

32-
def to_locale(language):
35+
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
36+
accept_language_re = re.compile(r'''
37+
([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*"
38+
(?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8"
39+
(?:\s*,\s*|$) # Multiple accepts per header.
40+
''', re.VERBOSE)
41+
42+
def to_locale(language, to_lower=False):
3343
"Turns a language name (en-us) into a locale name (en_US)."
3444
p = language.find('-')
3545
if p >= 0:
36-
return language[:p].lower()+'_'+language[p+1:].upper()
46+
if to_lower:
47+
return language[:p].lower()+'_'+language[p+1:].lower()
48+
else:
49+
return language[:p].lower()+'_'+language[p+1:].upper()
3750
else:
3851
return language.lower()
3952

@@ -309,46 +322,40 @@ def get_language_from_request(request):
309322
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
310323
return lang_code
311324

312-
lang_code = request.COOKIES.get('django_language', None)
313-
if lang_code in supported and lang_code is not None and check_for_language(lang_code):
325+
lang_code = request.COOKIES.get('django_language')
326+
if lang_code and lang_code in supported and check_for_language(lang_code):
314327
return lang_code
315328

316-
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
317-
if accept is not None:
318-
319-
t = _accepted.get(accept, None)
320-
if t is not None:
321-
return t
322-
323-
def _parsed(el):
324-
p = el.find(';q=')
325-
if p >= 0:
326-
lang = el[:p].strip()
327-
order = int(float(el[p+3:].strip())*100)
328-
else:
329-
lang = el
330-
order = 100
331-
p = lang.find('-')
332-
if p >= 0:
333-
mainlang = lang[:p]
334-
else:
335-
mainlang = lang
336-
return (lang, mainlang, order)
337-
338-
langs = [_parsed(el) for el in accept.split(',')]
339-
langs.sort(lambda a,b: -1*cmp(a[2], b[2]))
340-
341-
for lang, mainlang, order in langs:
342-
if lang in supported or mainlang in supported:
343-
langfile = gettext_module.find('django', globalpath, [to_locale(lang)])
344-
if langfile:
345-
# reconstruct the actual language from the language
346-
# filename, because otherwise we might incorrectly
347-
# report de_DE if we only have de available, but
348-
# did find de_DE because of language normalization
349-
lang = langfile[len(globalpath):].split(os.path.sep)[1]
350-
_accepted[accept] = lang
351-
return lang
329+
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
330+
for lang, unused in parse_accept_lang_header(accept):
331+
if lang == '*':
332+
break
333+
334+
# We have a very restricted form for our language files (no encoding
335+
# specifier, since they all must be UTF-8 and only one possible
336+
# language each time. So we avoid the overhead of gettext.find() and
337+
# look up the MO file manually.
338+
339+
normalized = locale.locale_alias.get(to_locale(lang, True))
340+
if not normalized:
341+
continue
342+
343+
# Remove the default encoding from locale_alias
344+
normalized = normalized.split('.')[0]
345+
346+
if normalized in _accepted:
347+
# We've seen this locale before and have an MO file for it, so no
348+
# need to check again.
349+
return _accepted[normalized]
350+
351+
for lang in (normalized, normalized.split('_')[0]):
352+
if lang not in supported:
353+
continue
354+
langfile = os.path.join(globalpath, lang, 'LC_MESSAGES',
355+
'django.mo')
356+
if os.path.exists(langfile):
357+
_accepted[normalized] = lang
358+
return lang
352359

353360
return settings.LANGUAGE_CODE
354361

@@ -494,3 +501,24 @@ def string_concat(*strings):
494501
return ''.join([str(el) for el in strings])
495502

496503
string_concat = lazy(string_concat, str)
504+
505+
def parse_accept_lang_header(lang_string):
506+
"""
507+
Parses the lang_string, which is the body of an HTTP Accept-Language
508+
header, and returns a list of (lang, q-value), ordered by 'q' values.
509+
510+
Any format errors in lang_string results in an empty list being returned.
511+
"""
512+
result = []
513+
pieces = accept_language_re.split(lang_string)
514+
if pieces[-1]:
515+
return []
516+
for i in range(0, len(pieces) - 1, 3):
517+
first, lang, priority = pieces[i : i + 3]
518+
if first:
519+
return []
520+
priority = priority and float(priority) or 1.0
521+
result.append((lang, priority))
522+
result.sort(lambda x, y: -cmp(x[1], y[1]))
523+
return result
524+

docs/release_notes_0.96.txt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
=================================
2-
Django version 0.96 release notes
3-
=================================
1+
===================================
2+
Django version 0.96.1 release notes
3+
===================================
44

5-
Welcome to Django 0.96!
5+
Welcome to Django 0.96.1!
66

77
The primary goal for 0.96 is a cleanup and stabilization of the features
88
introduced in 0.95. There have been a few small `backwards-incompatible
9-
changes`_ since 0.95, but the upgrade process should be fairly simple
9+
changes since 0.95`_, but the upgrade process should be fairly simple
1010
and should not require major changes to existing applications.
1111

1212
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
1717
instead of needing to make incremental changes to keep up with the
1818
development version of Django.
1919

20-
Backwards-incompatible changes
20+
Changes since the 0.96 release
2121
==============================
2222

23+
This release contains fixes for a security vulnerability discovered after the
24+
initial release of Django 0.96. A bug in the i18n framework could allow an
25+
attacker to send extremely large strings in the Accept-Language header and
26+
cause a denial of service by filling available memory.
27+
28+
Because this problems wasn't discovered and fixed until after the 0.96
29+
release, it's recommended that you use this release rather than the original
30+
0.96.
31+
32+
Backwards-incompatible changes since 0.95
33+
=========================================
34+
2335
The following changes may require you to update your code when you switch from
2436
0.95 to 0.96:
2537

setup.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,9 @@
3232
for file_info in data_files:
3333
file_info[0] = '/PURELIB/%s' % file_info[0]
3434

35-
# Dynamically calculate the version based on django.VERSION.
36-
version = "%d.%d-%s" % (__import__('django').VERSION)
37-
3835
setup(
3936
name = "Django",
40-
version = version,
37+
version = "0.96.1",
4138
url = 'http://www.djangoproject.com/',
4239
author = 'Lawrence Journal-World',
4340
author_email = 'holovaty@gmail.com',

0 commit comments

Comments
 (0)