Disallow surrogate escapes in dicts and lists in the config

In Dict.to_str() and List.to_str() we use json.dump to get a value. However,
JSON includes surrogate escapes in the dumped values, which breaks round trips.

>>> yaml.load(json.dumps({'\U00010000': True}))
{'\ud800\udc00': True}

>>> yaml.load(json.dumps({'\U00010000': True}, ensure_ascii=False))
yaml.reader.ReaderError: unacceptable character #x10000: special characters are not allowed

See:
https://stackoverflow.com/a/38552626/2085149
https://news.ycombinator.com/item?id=12798032
This commit is contained in:
Florian Bruhin 2017-07-04 10:16:27 +02:00
parent fa0f4e1101
commit 9ac2dbcc80
2 changed files with 22 additions and 1 deletions

View File

@ -181,6 +181,17 @@ class BaseType:
raise configexc.ValidationError(
value, "may not contain unprintable chars!")
def _validate_surrogate_escapes(self, full_value, value):
"""Make sure the given value doesn't contain surrogate escapes.
This is used for values passed to json.dump, as it can't handle those.
"""
if not isinstance(value, str):
return
if any(ord(c) > 0xFFFF for c in value):
raise configexc.ValidationError(
full_value, "may not contain surrogate escapes!")
def _validate_valid_values(self, value):
"""Validate value against possible values.
@ -418,6 +429,9 @@ class List(BaseType):
if not value:
return []
for val in value:
self._validate_surrogate_escapes(value, val)
if self.length is not None and len(value) != self.length:
raise configexc.ValidationError(value, "Exactly {} values need to "
"be set!".format(self.length))
@ -1089,6 +1103,9 @@ class Dict(BaseType):
return self._fill_fixed_keys({})
self._validate_keys(value)
for key, val in value.items():
self._validate_surrogate_escapes(value, key)
self._validate_surrogate_escapes(value, val)
d = {self.keytype.to_py(key): self.valtype.to_py(val)
for key, val in value.items()}

View File

@ -429,6 +429,8 @@ class TestString:
({'minlen': 2, 'maxlen': 3}, 'abc'),
# valid_values
({'valid_values': configtypes.ValidValues('abcd')}, 'abcd'),
# Surrogate escapes are allowed in strings
({}, '\U00010000'),
])
def test_to_py(self, klass, kwargs, val):
assert klass(**kwargs).to_py(val) == val
@ -535,7 +537,7 @@ class TestList:
def test_to_py(self, klass, val):
assert klass().to_py(val) == val
@pytest.mark.parametrize('val', [[42], '["foo"]'])
@pytest.mark.parametrize('val', [[42], '["foo"]', ['\U00010000']])
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().to_py(val)
@ -1427,6 +1429,8 @@ class TestDict:
assert klass(keytype=keytype, valtype=valtype).to_py(val) == val
@pytest.mark.parametrize('val', [
{'\U00010000': 'foo'}, # UTF-16 surrogate in key
{'foo': '\U00010000'}, # UTF-16 surrogate in value
{0: 'foo'}, # Invalid key type
{'foo': 0}, # Invalid value type
])