Description
yaml.safe_load raises raw KeyError / ValueError / IndexError (instead of yaml.YAMLError / ConstructorError) when a YAML document uses an explicit type tag (!!bool, !!int, !!float) with a value that doesn't match the expected format.
Applications that catch yaml.YAMLError to gracefully handle untrusted input will let these exceptions propagate, crashing the caller.
Reproducer (pyyaml 6.0.3, latest on PyPI)
import yaml
yaml.safe_load('!!bool "maybe"')
# KeyError: 'maybe'
# in constructor.py:235, construct_yaml_bool:
# return self.bool_values[value.lower()]
yaml.safe_load('!!int "abc"')
# ValueError: invalid literal for int() with base 10: 'abc'
yaml.safe_load('!!int ""')
# IndexError: string index out of range
yaml.safe_load('!!float "abc"')
# ValueError: could not convert string to float: 'abc'
Expected behavior
All of the above should raise yaml.constructor.ConstructorError (a subclass of yaml.YAMLError), which is what callers wrap yaml.safe_load in try/except for. The constructor functions should catch the KeyError/ValueError from int() / float() / dict lookup and re-raise as ConstructorError.
Related: bool case-sensitivity differential
While investigating I also noticed a differential: the YAML 1.1 resolver regex is case-sensitive for the limited set yes|Yes|YES|y|... but construct_yaml_bool lowercases the value before lookup, so !!bool "yeS" is accepted as True while plain yeS is treated as a string.
yaml.safe_load('yeS') # -> 'yeS' (correct, untagged → string)
yaml.safe_load('!!bool "yeS"') # -> True (inconsistent: explicit tag normalizes case)
This is less severe but is the same root cause (constructor handling is more permissive than the resolver expects).
Suggested fix
In lib/yaml/constructor.py:
def construct_yaml_bool(self, node):
value = self.construct_scalar(node)
- return self.bool_values[value.lower()]
+ try:
+ return self.bool_values[value.lower()]
+ except KeyError:
+ raise ConstructorError(None, None,
+ f"could not determine a constructor for the tag {node.tag!r} with value {value!r}",
+ node.start_mark)
Similar wrapping in construct_yaml_int, construct_yaml_float, construct_yaml_null.
Description
yaml.safe_loadraises rawKeyError/ValueError/IndexError(instead ofyaml.YAMLError/ConstructorError) when a YAML document uses an explicit type tag (!!bool,!!int,!!float) with a value that doesn't match the expected format.Applications that catch
yaml.YAMLErrorto gracefully handle untrusted input will let these exceptions propagate, crashing the caller.Reproducer (pyyaml 6.0.3, latest on PyPI)
Expected behavior
All of the above should raise
yaml.constructor.ConstructorError(a subclass ofyaml.YAMLError), which is what callers wrapyaml.safe_loadintry/exceptfor. The constructor functions should catch theKeyError/ValueErrorfromint()/float()/ dict lookup and re-raise asConstructorError.Related: bool case-sensitivity differential
While investigating I also noticed a differential: the YAML 1.1 resolver regex is case-sensitive for the limited set
yes|Yes|YES|y|...butconstruct_yaml_boollowercases the value before lookup, so!!bool "yeS"is accepted asTruewhile plainyeSis treated as a string.This is less severe but is the same root cause (constructor handling is more permissive than the resolver expects).
Suggested fix
In
lib/yaml/constructor.py:def construct_yaml_bool(self, node): value = self.construct_scalar(node) - return self.bool_values[value.lower()] + try: + return self.bool_values[value.lower()] + except KeyError: + raise ConstructorError(None, None, + f"could not determine a constructor for the tag {node.tag!r} with value {value!r}", + node.start_mark)Similar wrapping in
construct_yaml_int,construct_yaml_float,construct_yaml_null.