Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean locale identifiers before loading from file #782

Merged
merged 2 commits into from Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)