From a4706b81ebbdf8571779182cd4e303910cf452b8 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:52:40 +0300 Subject: [PATCH] Reject trailing characters in hashes and encoded values --- src/validators/encoding.py | 8 ++++---- src/validators/hashes.py | 12 ++++++------ tests/test_encoding.py | 15 +++++++++++++++ tests/test_hashes.py | 15 +++++++++++++++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/validators/encoding.py b/src/validators/encoding.py index 2cb7c47a..c4ec2cea 100644 --- a/src/validators/encoding.py +++ b/src/validators/encoding.py @@ -25,7 +25,7 @@ def base16(value: str, /): (Literal[True]): If `value` is a valid base16 encoding. (ValidationError): If `value` is an invalid base16 encoding. """ - return re.match(r"^[0-9A-Fa-f]+$", value) if value else False + return re.fullmatch(r"^[0-9A-Fa-f]+$", value) if value else False @validator @@ -46,7 +46,7 @@ def base32(value: str, /): (Literal[True]): If `value` is a valid base32 encoding. (ValidationError): If `value` is an invalid base32 encoding. """ - return re.match(r"^[A-Z2-7]+=*$", value) if value else False + return re.fullmatch(r"^[A-Z2-7]+=*$", value) if value else False @validator @@ -67,7 +67,7 @@ def base58(value: str, /): (Literal[True]): If `value` is a valid base58 encoding. (ValidationError): If `value` is an invalid base58 encoding. """ - return re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", value) if value else False + return re.fullmatch(r"^[1-9A-HJ-NP-Za-km-z]+$", value) if value else False @validator @@ -89,7 +89,7 @@ def base64(value: str, /): (ValidationError): If `value` is an invalid base64 encoding. """ return ( - re.match(r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", value) + re.fullmatch(r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", value) if value else False ) diff --git a/src/validators/hashes.py b/src/validators/hashes.py index 2e9aee62..bd5035ad 100644 --- a/src/validators/hashes.py +++ b/src/validators/hashes.py @@ -25,7 +25,7 @@ def md5(value: str, /): (Literal[True]): If `value` is a valid MD5 hash. (ValidationError): If `value` is an invalid MD5 hash. """ - return re.match(r"^[0-9a-f]{32}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{32}$", value, re.IGNORECASE) if value else False @validator @@ -46,7 +46,7 @@ def sha1(value: str, /): (Literal[True]): If `value` is a valid SHA1 hash. (ValidationError): If `value` is an invalid SHA1 hash. """ - return re.match(r"^[0-9a-f]{40}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{40}$", value, re.IGNORECASE) if value else False @validator @@ -67,7 +67,7 @@ def sha224(value: str, /): (Literal[True]): If `value` is a valid SHA224 hash. (ValidationError): If `value` is an invalid SHA224 hash. """ - return re.match(r"^[0-9a-f]{56}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{56}$", value, re.IGNORECASE) if value else False @validator @@ -91,7 +91,7 @@ def sha256(value: str, /): (Literal[True]): If `value` is a valid SHA256 hash. (ValidationError): If `value` is an invalid SHA256 hash. """ - return re.match(r"^[0-9a-f]{64}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{64}$", value, re.IGNORECASE) if value else False @validator @@ -115,7 +115,7 @@ def sha384(value: str, /): (Literal[True]): If `value` is a valid SHA384 hash. (ValidationError): If `value` is an invalid SHA384 hash. """ - return re.match(r"^[0-9a-f]{96}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{96}$", value, re.IGNORECASE) if value else False @validator @@ -140,4 +140,4 @@ def sha512(value: str, /): (Literal[True]): If `value` is a valid SHA512 hash. (ValidationError): If `value` is an invalid SHA512 hash. """ - return re.match(r"^[0-9a-f]{128}$", value, re.IGNORECASE) if value else False + return re.fullmatch(r"^[0-9a-f]{128}$", value, re.IGNORECASE) if value else False diff --git a/tests/test_encoding.py b/tests/test_encoding.py index db5bccf8..9728a8fe 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -1,5 +1,8 @@ """Test Encodings.""" +# standard +from typing import Callable + # external import pytest @@ -115,3 +118,15 @@ def test_returns_true_on_valid_base64(value: str): def test_returns_failed_validation_on_invalid_base64(value: str): """Test returns failed validation on invalid base64.""" assert isinstance(base64(value), ValidationError) + + +@pytest.mark.parametrize( + "validate, value", + [(base16, "a3f4b2"), (base32, "MFZWIZLTOQ======"), (base58, "18KToMF5"), (base64, "SGVsbG8=")], +) +@pytest.mark.parametrize("suffix", ["\n", "\r\n", " ", "\nextra"]) +def test_encoding_rejects_trailing_characters( + validate: Callable[[str], object], value: str, suffix: str +): + """Reject characters outside the encoded value.""" + assert isinstance(validate(value + suffix), ValidationError) diff --git a/tests/test_hashes.py b/tests/test_hashes.py index 64f0affc..6e78260e 100644 --- a/tests/test_hashes.py +++ b/tests/test_hashes.py @@ -1,5 +1,8 @@ """Test Hashes.""" +# standard +from typing import Callable + # external import pytest @@ -229,3 +232,15 @@ def test_returns_true_on_valid_sha512(value: str): def test_returns_failed_validation_on_invalid_sha512(value: str): """Test returns failed validation on invalid sha512.""" assert isinstance(sha512(value), ValidationError) + + +@pytest.mark.parametrize( + "validate, size", + [(md5, 32), (sha1, 40), (sha224, 56), (sha256, 64), (sha384, 96), (sha512, 128)], +) +@pytest.mark.parametrize("suffix", ["\n", "\r\n", " ", "\nextra"]) +def test_hash_rejects_trailing_characters( + validate: Callable[[str], object], size: int, suffix: str +): + """Reject characters outside the fixed-length digest.""" + assert isinstance(validate("a" * size + suffix), ValidationError)