Skip to content

safe_load raises raw KeyError/ValueError/IndexError instead of ConstructorError for !!bool/!!int/!!float with invalid values #933

Description

@zhangjiashuo-cs

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions