From 8dba811031b44adf32fa00d722ce3fae18b6bf6f Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 20 Jun 2026 14:51:27 +0200 Subject: [PATCH] Return ValidationError for non-string input instead of leaking AttributeError Validators that reach for string methods on the value -- uuid via UUID(value) -> value.replace, email/hostname via value.count, cron via value.strip -- raise AttributeError when given a non-string (int, float, bool, list, dict). The validator decorator only converted (ValueError, TypeError, UnicodeError) to ValidationError, so AttributeError escaped uncaught: a caller passing untrusted or unknown-typed data to a validator got a crash instead of the documented True/ValidationError result, and inconsistently with the many validators that already return ValidationError for wrong-typed input. Add AttributeError to the decorator's caught set so this whole class of validators handles non-string input uniformly. --- src/validators/utils.py | 2 +- tests/test_validation_failure.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/validators/utils.py b/src/validators/utils.py index 28d3c857..94ce7226 100644 --- a/src/validators/utils.py +++ b/src/validators/utils.py @@ -91,7 +91,7 @@ def wrapper(*args: Any, **kwargs: Any): if func(*args, **kwargs) else ValidationError(func, _func_args_as_dict(func, *args, **kwargs)) ) - except (ValueError, TypeError, UnicodeError) as exp: + except (ValueError, TypeError, UnicodeError, AttributeError) as exp: if raise_validation_error: raise ValidationError( func, _func_args_as_dict(func, *args, **kwargs), str(exp) diff --git a/tests/test_validation_failure.py b/tests/test_validation_failure.py index 67502000..86e747b8 100644 --- a/tests/test_validation_failure.py +++ b/tests/test_validation_failure.py @@ -1,7 +1,11 @@ """Test validation Failure.""" +# external +import pytest + # local -from validators import between +from validators import between, cron, email, hostname, uuid +from validators.utils import ValidationError failed_obj_repr = "ValidationError(func=between" @@ -31,3 +35,16 @@ def test_arguments_as_properties(self): assert self.is_in_between.__dict__["value"] == 3 assert self.is_in_between.__dict__["min_val"] == 4 assert self.is_in_between.__dict__["max_val"] == 5 + + +@pytest.mark.parametrize("validator", [uuid, email, hostname, cron]) +@pytest.mark.parametrize("value", [123, 1.5, True, ["x"], {"a": 1}]) +def test_returns_validation_error_on_non_string_input(validator, value): + """Wrong-typed input returns ValidationError, not a leaked exception. + + These validators reach for string methods (e.g. ``.replace``/``.count``/ + ``.strip``) on the value, which raises ``AttributeError`` for non-strings. + The decorator must convert that into a ``ValidationError`` like every other + invalid input, rather than letting it escape. + """ + assert isinstance(validator(value), ValidationError)