Skip to content

Commit 5efa15f

Browse files
Copilotstephentoub
andauthored
Convert.FromBase64XYZ decoder should reject input when unused bits are not set to 0 (#121044)
Implements RFC 4648 Section 3.5 compliance by rejecting Base64 input where unused bits are not set to zero. This ensures that decoding is deterministic—only one valid encoding exists for each byte sequence. Fixes #105262 --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]>
1 parent 7823f50 commit 5efa15f

File tree

3 files changed

+86
-13
lines changed

3 files changed

+86
-13
lines changed

src/libraries/System.Private.CoreLib/src/System/Convert.Base64.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private static bool TryDecodeFromUtf16(ReadOnlySpan<char> utf16, Span<byte> byte
119119

120120
i0 |= i2;
121121

122-
if (i0 < 0)
122+
if ((i0 & 0x800000c0) != 0) // if negative or 2 unused bits are not 0.
123123
goto InvalidExit;
124124
if (destIndex > destLength - 2)
125125
goto InvalidExit;
@@ -129,7 +129,7 @@ private static bool TryDecodeFromUtf16(ReadOnlySpan<char> utf16, Span<byte> byte
129129
}
130130
else
131131
{
132-
if (i0 < 0)
132+
if ((i0 & 0x8000F000) != 0) // if negative or 4 unused bits are not 0.
133133
goto InvalidExit;
134134
if (destIndex > destLength - 1)
135135
goto InvalidExit;

src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.FromBase64.cs

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,29 +76,36 @@ public static void RoundtripWithPadding2()
7676
[Fact]
7777
public static void PartialRoundtripWithPadding1()
7878
{
79+
// "ab==" has non-zero unused bits and should be rejected per RFC 4648
80+
// The valid encoding for the same byte should be "aQ=="
7981
string input = "ab==";
80-
Verify(input, result =>
82+
VerifyInvalidInput(input);
83+
84+
// Test the valid encoding instead
85+
string validInput = "aQ==";
86+
Verify(validInput, result =>
8187
{
8288
Assert.Equal(1, result.Length);
83-
8489
string roundtrippedString = Convert.ToBase64String(result);
85-
Assert.NotEqual(input, roundtrippedString);
86-
Assert.Equal(input[0], roundtrippedString[0]);
90+
Assert.Equal(validInput, roundtrippedString);
8791
});
8892
}
8993

9094
[Fact]
9195
public static void PartialRoundtripWithPadding2()
9296
{
97+
// "789=" has non-zero unused bits and should be rejected per RFC 4648
98+
// The valid encoding for the same 2 bytes should be "788="
9399
string input = "789=";
94-
Verify(input, result =>
100+
VerifyInvalidInput(input);
101+
102+
// Test the valid encoding instead
103+
string validInput = "788=";
104+
Verify(validInput, result =>
95105
{
96106
Assert.Equal(2, result.Length);
97-
98107
string roundtrippedString = Convert.ToBase64String(result);
99-
Assert.NotEqual(input, roundtrippedString);
100-
Assert.Equal(input[0], roundtrippedString[0]);
101-
Assert.Equal(input[1], roundtrippedString[1]);
108+
Assert.Equal(validInput, roundtrippedString);
102109
});
103110
}
104111

@@ -266,5 +273,71 @@ private static void Verify(string input, Action<byte[]> action = null)
266273
action(Convert.FromBase64String(input));
267274
}
268275
}
276+
277+
[Fact]
278+
public static void RejectsInvalidUnusedBits_OnePadding()
279+
{
280+
// When there's one padding character (2 bytes output), the last 2 bits must be 0
281+
// "QUI=" is valid (encodes "AB"), but variations with non-zero unused bits should fail
282+
string validInput = "QUI=";
283+
byte[] result = Convert.FromBase64String(validInput);
284+
Assert.Equal(2, result.Length);
285+
Assert.Equal(65, result[0]); // 'A'
286+
Assert.Equal(66, result[1]); // 'B'
287+
288+
// These have non-zero unused bits and should be rejected
289+
VerifyInvalidInput("QUJ="); // last 2 bits != 0
290+
VerifyInvalidInput("QUK="); // last 2 bits != 0
291+
VerifyInvalidInput("QUL="); // last 2 bits != 0
292+
}
293+
294+
[Fact]
295+
public static void RejectsInvalidUnusedBits_TwoPadding()
296+
{
297+
// When there are two padding characters (1 byte output), the last 4 bits must be 0
298+
// "QQ==" is valid (encodes "A"), but variations with non-zero unused bits should fail
299+
string validInput = "QQ==";
300+
byte[] result = Convert.FromBase64String(validInput);
301+
Assert.Equal(1, result.Length);
302+
Assert.Equal(65, result[0]); // 'A'
303+
304+
// These have non-zero unused bits and should be rejected
305+
VerifyInvalidInput("QR=="); // last 4 bits != 0
306+
VerifyInvalidInput("QS=="); // last 4 bits != 0
307+
VerifyInvalidInput("QT=="); // last 4 bits != 0
308+
VerifyInvalidInput("QU=="); // last 4 bits != 0
309+
VerifyInvalidInput("QV=="); // last 4 bits != 0
310+
VerifyInvalidInput("QW=="); // last 4 bits != 0
311+
VerifyInvalidInput("QX=="); // last 4 bits != 0
312+
VerifyInvalidInput("QY=="); // last 4 bits != 0
313+
VerifyInvalidInput("QZ=="); // last 4 bits != 0
314+
VerifyInvalidInput("Qa=="); // last 4 bits != 0
315+
VerifyInvalidInput("Qb=="); // last 4 bits != 0
316+
VerifyInvalidInput("Qc=="); // last 4 bits != 0
317+
VerifyInvalidInput("Qd=="); // last 4 bits != 0
318+
VerifyInvalidInput("Qe=="); // last 4 bits != 0
319+
VerifyInvalidInput("Qf=="); // last 4 bits != 0
320+
}
321+
322+
[Fact]
323+
public static void AcceptsValidUnusedBits()
324+
{
325+
// Valid cases with zero unused bits should continue to work
326+
327+
// No padding - all bits used
328+
string noPadding = "QUJD"; // "ABC"
329+
byte[] result1 = Convert.FromBase64String(noPadding);
330+
Assert.Equal(3, result1.Length);
331+
332+
// One padding - last 2 bits are 0
333+
string onePadding = "QUI="; // "AB"
334+
byte[] result2 = Convert.FromBase64String(onePadding);
335+
Assert.Equal(2, result2.Length);
336+
337+
// Two padding - last 4 bits are 0
338+
string twoPadding = "QQ=="; // "A"
339+
byte[] result3 = Convert.FromBase64String(twoPadding);
340+
Assert.Equal(1, result3.Length);
341+
}
269342
}
270343
}

src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Convert.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,8 @@ public static IEnumerable<Tuple<string, byte[]>> Base64TestDataSeed
399399
// All whitespace characters.
400400
yield return Tuple.Create<string, byte[]>(" \t\r\n", Array.Empty<byte>());
401401

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

406406
// Typical

0 commit comments

Comments
 (0)