From 10ee7c380012a736317bd3efb89c8f8dbdb535c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:28:23 +0000 Subject: [PATCH] [patch] Reject whitespace in [IsBase64] regardless of total length Fixes ktsu-dev/Semantics#316 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019RYN9jT6szUiySkywFKCA1 --- .../Attributes/Text/IsBase64Attribute.cs | 33 +++++++++++++++++++ .../TextValidationAttributesTests.cs | 26 +++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Semantics.Strings/Validation/Attributes/Text/IsBase64Attribute.cs b/Semantics.Strings/Validation/Attributes/Text/IsBase64Attribute.cs index 996cdf89..5ec88834 100644 --- a/Semantics.Strings/Validation/Attributes/Text/IsBase64Attribute.cs +++ b/Semantics.Strings/Validation/Attributes/Text/IsBase64Attribute.cs @@ -39,6 +39,14 @@ protected override ValidationResult ValidateValue(string value) return ValidationResult.Failure("The value must be a valid Base64 string."); } + // Convert.FromBase64String skips whitespace, which the length check above counts, so a + // value with whitespace would pass whenever it made the length a multiple of 4. Only the + // Base64 alphabet is accepted, with at most two '=' and only at the end. + if (!HasBase64Alphabet(value)) + { + return ValidationResult.Failure("The value must be a valid Base64 string."); + } + // Check for valid Base64 characters and proper padding try { @@ -50,5 +58,30 @@ protected override ValidationResult ValidateValue(string value) return ValidationResult.Failure("The value must be a valid Base64 string."); } } + + private static bool HasBase64Alphabet(string value) + { + int dataLength = value.Length; + while (dataLength > 0 && value[dataLength - 1] == '=') + { + dataLength--; + } + + if (value.Length - dataLength > 2) + { + return false; + } + + for (int i = 0; i < dataLength; i++) + { + char c = value[i]; + if (c is not ((>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or (>= '0' and <= '9') or '+' or '/')) + { + return false; + } + } + + return true; + } } } diff --git a/Semantics.Test/TextValidationAttributesTests.cs b/Semantics.Test/TextValidationAttributesTests.cs index 1e49d1db..e81f392d 100644 --- a/Semantics.Test/TextValidationAttributesTests.cs +++ b/Semantics.Test/TextValidationAttributesTests.cs @@ -144,9 +144,31 @@ public void IsBase64Attribute_InvalidBase64_ShouldThrow() } [TestMethod] - public void IsBase64Attribute_Base64WithSpaces_ShouldThrow() + [DataRow("SGVs bG8g V29y bGQ=")] + [DataRow("SGVs bG8g V29y bGQ= ")] + [DataRow("SGVsbG8gV29ybGQ= ")] + [DataRow("SGVsbG8g\r\nV29ybGQ=\r\n")] + [DataRow("SGVsbG8gV29ybGQ=\t\t\t\t")] + public void IsBase64Attribute_Base64WithWhitespace_ShouldThrow(string value) { - Assert.ThrowsExactly(() => SemanticString.Create("SGVs bG8g V29y bGQ=")); + Assert.ThrowsExactly(() => SemanticString.Create(value)); + } + + [TestMethod] + [DataRow("VGVzdA=a")] + [DataRow("VGV=dA==")] + [DataRow("VGVzd===")] + public void IsBase64Attribute_MisplacedPadding_ShouldThrow(string value) + { + Assert.ThrowsExactly(() => SemanticString.Create(value)); + } + + [TestMethod] + [DataRow("+/+/")] + [DataRow("AB+/CD==")] + public void IsBase64Attribute_PlusAndSlash_ShouldPass(string value) + { + Assert.AreEqual(value, SemanticString.Create(value).WeakString); } [TestMethod]