Skip to content

Commit d89111a

Browse files
committed
harden localedata loading with restricted unpickler
1 parent baf9431 commit d89111a

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

babel/localedata.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@ def _is_non_likely_script(name: str) -> bool:
118118
return False
119119

120120

121+
class _SafeUnpickler(pickle.Unpickler):
122+
def find_class(self, module, name):
123+
if module == 'babel.localedata' and name == 'Alias':
124+
return Alias
125+
if module == 'babel.plural' and name == 'PluralRule':
126+
from babel.plural import PluralRule
127+
return PluralRule
128+
if module == 'decimal' and name == 'Decimal':
129+
from decimal import Decimal
130+
return Decimal
131+
raise pickle.UnpicklingError(f'Global {module}.{name} is forbidden')
132+
133+
121134
def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
122135
"""Load the locale data for the given locale.
123136
@@ -165,9 +178,9 @@ def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str
165178
filename = resolve_locale_filename(name)
166179
with open(filename, 'rb') as fileobj:
167180
if name != 'root' and merge_inherited:
168-
merge(data, pickle.load(fileobj))
181+
merge(data, _SafeUnpickler(fileobj).load())
169182
else:
170-
data = pickle.load(fileobj)
183+
data = _SafeUnpickler(fileobj).load()
171184
_cache[name] = data
172185
return data
173186
finally:

tests/test_localedata_security.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import pickle
2+
import io
3+
import os
4+
import pytest
5+
from babel import localedata
6+
7+
def test_safe_unpickler_rejects_malicious_global():
8+
class Malicious:
9+
def __reduce__(self):
10+
return (os.system, ('echo VULNERABLE',))
11+
12+
buf = io.BytesIO()
13+
pickle.dump(Malicious(), buf)
14+
buf.seek(0)
15+
16+
unpickler = localedata._SafeUnpickler(buf)
17+
with pytest.raises(pickle.UnpicklingError) as excinfo:
18+
unpickler.load()
19+
assert 'forbidden' in str(excinfo.value)
20+
21+
def test_safe_unpickler_allows_legit_classes():
22+
from babel.plural import PluralRule
23+
from decimal import Decimal
24+
25+
data = {
26+
'alias': localedata.Alias(('en', 'US')),
27+
'plural': PluralRule({'one': 'n is 1'}),
28+
'decimal': Decimal('1.23')
29+
}
30+
31+
buf = io.BytesIO()
32+
pickle.dump(data, buf)
33+
buf.seek(0)
34+
35+
unpickler = localedata._SafeUnpickler(buf)
36+
loaded = unpickler.load()
37+
assert isinstance(loaded['alias'], localedata.Alias)
38+
assert isinstance(loaded['plural'], PluralRule)
39+
assert isinstance(loaded['decimal'], Decimal)

0 commit comments

Comments
 (0)