Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private static bool TryDecodeFromUtf16(ReadOnlySpan<char> utf16, Span<byte> byte

i0 |= i2;

if (i0 < 0)
if ((i0 & 0x800000c0) != 0) // if negative or 2 unused bits are not 0.
goto InvalidExit;
if (destIndex > destLength - 2)
goto InvalidExit;
Expand All @@ -129,7 +129,7 @@ private static bool TryDecodeFromUtf16(ReadOnlySpan<char> utf16, Span<byte> byte
}
else
{
if (i0 < 0)
if ((i0 & 0x8000F000) != 0) // if negative or 4 unused bits are not 0.
goto InvalidExit;
if (destIndex > destLength - 1)
goto InvalidExit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,36 @@ public static void RoundtripWithPadding2()
[Fact]
public static void PartialRoundtripWithPadding1()
{
// "ab==" has non-zero unused bits and should be rejected per RFC 4648
// The valid encoding for the same byte should be "aQ=="
string input = "ab==";
Verify(input, result =>
VerifyInvalidInput(input);

// Test the valid encoding instead
string validInput = "aQ==";
Verify(validInput, result =>
{
Assert.Equal(1, result.Length);

string roundtrippedString = Convert.ToBase64String(result);
Assert.NotEqual(input, roundtrippedString);
Assert.Equal(input[0], roundtrippedString[0]);
Assert.Equal(validInput, roundtrippedString);
});
}

[Fact]
public static void PartialRoundtripWithPadding2()
{
// "789=" has non-zero unused bits and should be rejected per RFC 4648
// The valid encoding for the same 2 bytes should be "788="
string input = "789=";
Verify(input, result =>
VerifyInvalidInput(input);

// Test the valid encoding instead
string validInput = "788=";
Verify(validInput, result =>
{
Assert.Equal(2, result.Length);

string roundtrippedString = Convert.ToBase64String(result);
Assert.NotEqual(input, roundtrippedString);
Assert.Equal(input[0], roundtrippedString[0]);
Assert.Equal(input[1], roundtrippedString[1]);
Assert.Equal(validInput, roundtrippedString);
});
}

Expand Down Expand Up @@ -266,5 +273,71 @@ private static void Verify(string input, Action<byte[]> action = null)
action(Convert.FromBase64String(input));
}
}

[Fact]
public static void RejectsInvalidUnusedBits_OnePadding()
{
// When there's one padding character (2 bytes output), the last 2 bits must be 0
// "QUI=" is valid (encodes "AB"), but variations with non-zero unused bits should fail
string validInput = "QUI=";
byte[] result = Convert.FromBase64String(validInput);
Assert.Equal(2, result.Length);
Assert.Equal(65, result[0]); // 'A'
Assert.Equal(66, result[1]); // 'B'

// These have non-zero unused bits and should be rejected
VerifyInvalidInput("QUJ="); // last 2 bits != 0
VerifyInvalidInput("QUK="); // last 2 bits != 0
VerifyInvalidInput("QUL="); // last 2 bits != 0
}

[Fact]
public static void RejectsInvalidUnusedBits_TwoPadding()
{
// When there are two padding characters (1 byte output), the last 4 bits must be 0
// "QQ==" is valid (encodes "A"), but variations with non-zero unused bits should fail
string validInput = "QQ==";
byte[] result = Convert.FromBase64String(validInput);
Assert.Equal(1, result.Length);
Assert.Equal(65, result[0]); // 'A'

// These have non-zero unused bits and should be rejected
VerifyInvalidInput("QR=="); // last 4 bits != 0
VerifyInvalidInput("QS=="); // last 4 bits != 0
VerifyInvalidInput("QT=="); // last 4 bits != 0
VerifyInvalidInput("QU=="); // last 4 bits != 0
VerifyInvalidInput("QV=="); // last 4 bits != 0
VerifyInvalidInput("QW=="); // last 4 bits != 0
VerifyInvalidInput("QX=="); // last 4 bits != 0
VerifyInvalidInput("QY=="); // last 4 bits != 0
VerifyInvalidInput("QZ=="); // last 4 bits != 0
VerifyInvalidInput("Qa=="); // last 4 bits != 0
VerifyInvalidInput("Qb=="); // last 4 bits != 0
VerifyInvalidInput("Qc=="); // last 4 bits != 0
VerifyInvalidInput("Qd=="); // last 4 bits != 0
VerifyInvalidInput("Qe=="); // last 4 bits != 0
VerifyInvalidInput("Qf=="); // last 4 bits != 0
}

[Fact]
public static void AcceptsValidUnusedBits()
{
// Valid cases with zero unused bits should continue to work

// No padding - all bits used
string noPadding = "QUJD"; // "ABC"
byte[] result1 = Convert.FromBase64String(noPadding);
Assert.Equal(3, result1.Length);

// One padding - last 2 bits are 0
string onePadding = "QUI="; // "AB"
byte[] result2 = Convert.FromBase64String(onePadding);
Assert.Equal(2, result2.Length);

// Two padding - last 4 bits are 0
string twoPadding = "QQ=="; // "A"
byte[] result3 = Convert.FromBase64String(twoPadding);
Assert.Equal(1, result3.Length);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,8 @@ public static IEnumerable<Tuple<string, byte[]>> Base64TestDataSeed
// All whitespace characters.
yield return Tuple.Create<string, byte[]>(" \t\r\n", Array.Empty<byte>());

// Pad characters
yield return Tuple.Create<string, byte[]>("BQYHCAZ=", "0506070806".HexToByteArray());
// Pad characters (using valid encodings with zero unused bits per RFC 4648)
yield return Tuple.Create<string, byte[]>("BQYHCAY=", "0506070806".HexToByteArray());
yield return Tuple.Create<string, byte[]>("BQYHCA==", "05060708".HexToByteArray());

// Typical
Expand Down
Loading