From 2ee0eabb4e59652a03250b839f7adfa5efce6096 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 17 Jul 2026 00:35:21 +0200 Subject: [PATCH 1/2] Fix bytes loader Base64 validation --- .../fragments/+strict_base64.bugfix.rst | 1 + .../_internal/morphing/concrete_provider.py | 9 +++++++++ tests/unit/morphing/test_concrete_provider.py | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 docs/changelog/fragments/+strict_base64.bugfix.rst diff --git a/docs/changelog/fragments/+strict_base64.bugfix.rst b/docs/changelog/fragments/+strict_base64.bugfix.rst new file mode 100644 index 00000000..9e13e2bf --- /dev/null +++ b/docs/changelog/fragments/+strict_base64.bugfix.rst @@ -0,0 +1 @@ +Reject leading and excess padding when loading Base64-encoded bytes-like values. diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index e910dcfb..65f9ad00 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -288,9 +288,18 @@ def bytes_base64_loader(data): encoded = data.encode("ascii") except AttributeError: raise TypeLoadError(str, data) + except UnicodeEncodeError: + raise ValueLoadError("Bad base64 string", data) if not B64_PATTERN.fullmatch(encoded): raise ValueLoadError("Bad base64 string", data) + if b"=" in encoded: + # a2b_base64 accepts leading and excess padding, so reject padding + # beyond the amount implied by the unpadded data length first. + unpadded = encoded.rstrip(b"=") + padding_length = len(encoded) - len(unpadded) + if not unpadded or padding_length > -len(unpadded) % 4: + raise ValueLoadError("Bad base64 string", data) try: return a2b_base64(encoded) diff --git a/tests/unit/morphing/test_concrete_provider.py b/tests/unit/morphing/test_concrete_provider.py index 126b6020..5b77230d 100644 --- a/tests/unit/morphing/test_concrete_provider.py +++ b/tests/unit/morphing/test_concrete_provider.py @@ -353,12 +353,20 @@ def test_bytes_like_provider( b64_string = b"YWJjZA==" assert get_string(loader(b64_string.decode())) == string + assert get_string(loader("")) == "" + assert get_string(loader("YQ==")) == "a" + assert get_string(loader("YWI=")) == "ab" raises_exc( ValueLoadError("Bad base64 string", "Hello, world"), lambda: loader("Hello, world"), ) + raises_exc( + ValueLoadError("Bad base64 string", "🦄"), + lambda: loader("🦄"), + ) + raises_exc( ValueLoadError( msg="Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4", @@ -372,6 +380,18 @@ def test_bytes_like_provider( lambda: loader("YWJjZA"), ) + for invalid_base64 in ( + "=", + "==", + "AAA==", + "AAAA=", + "AAAA==", + ): + raises_exc( + ValueLoadError("Bad base64 string", invalid_base64), + lambda invalid_base64=invalid_base64: loader(invalid_base64), + ) + raises_exc( TypeLoadError(str, 108), lambda: loader(108), From b30e8ec216e41defb1fa24a667b778de48e1f80c Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 17 Jul 2026 00:43:07 +0200 Subject: [PATCH 2/2] Deduplicate Base64 validation message --- src/adaptix/_internal/morphing/concrete_provider.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py index 65f9ad00..31f800ee 100644 --- a/src/adaptix/_internal/morphing/concrete_provider.py +++ b/src/adaptix/_internal/morphing/concrete_provider.py @@ -275,6 +275,7 @@ def provide_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> B64_PATTERN = re.compile(b"[A-Za-z0-9+/]*={0,2}") +BAD_BASE64_STRING = "Bad base64 string" @for_predicate(bytes) @@ -289,17 +290,17 @@ def bytes_base64_loader(data): except AttributeError: raise TypeLoadError(str, data) except UnicodeEncodeError: - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) if not B64_PATTERN.fullmatch(encoded): - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) if b"=" in encoded: # a2b_base64 accepts leading and excess padding, so reject padding # beyond the amount implied by the unpadded data length first. unpadded = encoded.rstrip(b"=") padding_length = len(encoded) - len(unpadded) if not unpadded or padding_length > -len(unpadded) % 4: - raise ValueLoadError("Bad base64 string", data) + raise ValueLoadError(BAD_BASE64_STRING, data) try: return a2b_base64(encoded)