Skip to content

Commit

Permalink
Merge pull request #782 from python-babel/locale-basename
Browse files Browse the repository at this point in the history
Clean locale identifiers before loading from file
  • Loading branch information
akx committed Apr 28, 2021
2 parents 5afe2b2 + 5caf717 commit 412015e
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 3 deletions.
24 changes: 22 additions & 2 deletions babel/localedata.py
Expand Up @@ -13,6 +13,8 @@
"""

import os
import re
import sys
import threading
from itertools import chain

Expand All @@ -22,6 +24,7 @@
_cache = {}
_cache_lock = threading.RLock()
_dirname = os.path.join(os.path.dirname(__file__), 'locale-data')
_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I)


def normalize_locale(name):
Expand All @@ -38,6 +41,22 @@ def normalize_locale(name):
return locale_id


def resolve_locale_filename(name):
"""
Resolve a locale identifier to a `.dat` path on disk.
"""

# Clean up any possible relative paths.
name = os.path.basename(name)

# Ensure we're not left with one of the Windows reserved names.
if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]):
raise ValueError("Name %s is invalid on Windows" % name)

# Build the path.
return os.path.join(_dirname, '%s.dat' % name)


def exists(name):
"""Check whether locale data is available for the given locale.
Expand All @@ -49,7 +68,7 @@ def exists(name):
return False
if name in _cache:
return True
file_found = os.path.exists(os.path.join(_dirname, '%s.dat' % name))
file_found = os.path.exists(resolve_locale_filename(name))
return True if file_found else bool(normalize_locale(name))


Expand Down Expand Up @@ -102,6 +121,7 @@ def load(name, merge_inherited=True):
:raise `IOError`: if no locale data file is found for the given locale
identifer, or one of the locales it inherits from
"""
name = os.path.basename(name)
_cache_lock.acquire()
try:
data = _cache.get(name)
Expand All @@ -119,7 +139,7 @@ def load(name, merge_inherited=True):
else:
parent = '_'.join(parts[:-1])
data = load(parent).copy()
filename = os.path.join(_dirname, '%s.dat' % name)
filename = resolve_locale_filename(name)
with open(filename, 'rb') as fileobj:
if name != 'root' and merge_inherited:
merge(data, pickle.load(fileobj))
Expand Down
39 changes: 38 additions & 1 deletion tests/test_localedata.py
Expand Up @@ -11,11 +11,17 @@
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://babel.edgewall.org/log/.

import os
import pickle
import sys
import tempfile
import unittest
import random
from operator import methodcaller

from babel import localedata
import pytest

from babel import localedata, Locale, UnknownLocaleError


class MergeResolveTestCase(unittest.TestCase):
Expand Down Expand Up @@ -131,3 +137,34 @@ def listdir_spy(*args):
localedata.locale_identifiers.cache = None
assert localedata.locale_identifiers()
assert len(listdir_calls) == 2


def test_locale_name_cleanup():
"""
Test that locale identifiers are cleaned up to avoid directory traversal.
"""
no_exist_name = os.path.join(tempfile.gettempdir(), "babel%d.dat" % random.randint(1, 99999))
with open(no_exist_name, "wb") as f:
pickle.dump({}, f)

try:
name = os.path.splitext(os.path.relpath(no_exist_name, localedata._dirname))[0]
except ValueError:
if sys.platform == "win32":
pytest.skip("unable to form relpath")
raise

assert not localedata.exists(name)
with pytest.raises(IOError):
localedata.load(name)
with pytest.raises(UnknownLocaleError):
Locale(name)


@pytest.mark.skipif(sys.platform != "win32", reason="windows-only test")
def test_reserved_locale_names():
for name in ("con", "aux", "nul", "prn", "com8", "lpt5"):
with pytest.raises(ValueError):
localedata.load(name)
with pytest.raises(ValueError):
Locale(name)

0 comments on commit 412015e

Please sign in to comment.