Skip to content

Commit 4f257b8

Browse files
authored
Gracefully reject malformed API keys (#8846)
We revoke API keys leaked on GitHub. The leak detection is a best effort and may discover malformed API keys, like API keys that aren't properly base32 encoded. Currently these malformed API keys will cause the admin panel to respond with HTTP 500. We should gracefully reject these malformed API keys. Addresses #8845
1 parent c63bb49 commit 4f257b8

4 files changed

Lines changed: 95 additions & 35 deletions

File tree

src/NuGetGallery.Services/Authentication/ApiKeyV4.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,16 @@ private bool TryParseInternal(string plaintextApiKey)
121121
try
122122
{
123123
var id = plaintextApiKey.Substring(0, IdPartBase32Length);
124-
var idBytes = id.AppendBase32Padding().ToUpper().FromBase32String();
124+
var validId = id
125+
.AppendBase32Padding()
126+
.ToUpper()
127+
.TryDecodeBase32String(out var idBytes);
128+
129+
if (!validId)
130+
{
131+
return false;
132+
}
133+
125134
bool success = idBytes[0] == IdPrefix[0] && idBytes[1] == IdPrefix[1];
126135

127136
if (success)

src/NuGetGallery.Services/Extensions/Base32Encoder.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ public static string ToBase32String(this byte[] data)
1818
return Encode(data);
1919
}
2020

21+
public static bool TryDecodeBase32String(this string base32String, out byte[] result)
22+
{
23+
try
24+
{
25+
result = Decode(base32String);
26+
return true;
27+
}
28+
catch (ArgumentException)
29+
{
30+
result = Array.Empty<byte>();
31+
return false;
32+
}
33+
}
34+
2135
public static byte[] FromBase32String(this string base32String)
2236
{
2337
return Decode(base32String);
@@ -42,7 +56,7 @@ public static string Encode(byte[] data)
4256
{
4357
if (data == null)
4458
{
45-
throw new NullReferenceException(nameof(data));
59+
throw new ArgumentNullException(nameof(data));
4660
}
4761

4862
int ncTokens = GetTokenCount(data);
@@ -63,7 +77,7 @@ public static byte[] Decode(string base32String)
6377
{
6478
if (base32String == null)
6579
{
66-
throw new NullReferenceException(nameof(base32String));
80+
throw new ArgumentNullException(nameof(base32String));
6781
}
6882

6983
// Validate base32 format

tests/NuGetGallery.Facts/Infrastructure/Authentication/ApiKeyV4Facts.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ public void CreatesAValidApiKey()
3535
[InlineData(" ")]
3636
[InlineData("abc")]
3737
[InlineData("SEMTXET5UU6UZDD4AMK57TR46I==")]
38+
[InlineData("0000thisis46charactersbutnotvalidbase32encoded")]
3839
public void TryParseFailsForIllegalApiKeys(string inputApiKey)
3940
{
40-
// Act
41+
// Act
4142
bool result = ApiKeyV4.TryParse(inputApiKey, out var apiKey);
4243

4344
// Assert

tests/NuGetGallery.Facts/Infrastructure/Authentication/Base32EncoderTests.cs

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
67
using System.Text;
78
using NuGetGallery.Infrastructure.Authentication;
@@ -13,55 +14,84 @@ public class Base32EncoderTests
1314
{
1415
private Random _random = new Random(0);
1516

17+
// Maps string to its base32 encoding
18+
public static IEnumerable<object[]> ValidBase32 => new List<object[]>
19+
{
20+
new object[] { "", "" },
21+
new object[] { "f", "MY======" },
22+
new object[] { "fo", "MZXQ====" },
23+
new object[] { "foo", "MZXW6===" },
24+
new object[] { "foob", "MZXW6YQ=" },
25+
new object[] { "fooba", "MZXW6YTB" },
26+
new object[] { "foobar", "MZXW6YTBOI======" },
27+
};
28+
29+
public static IEnumerable<object[]> InvalidBase32 => new List<object[]>
30+
{
31+
new object[] { "a" },
32+
new object[] { "abc" },
33+
new object[] { "SEMTXET5UU6UZDD4AMK57TR46I==" },
34+
new object[] { "mjwgc===" },
35+
new object[] { "GEYq====" },
36+
};
37+
38+
[Fact]
39+
public void EncodeThrowsNull()
40+
{
41+
Assert.Throws<ArgumentNullException>(() => Base32Encoder.Encode(data: null));
42+
}
43+
1644
[Fact]
17-
public void WhenDataIsNullEncodeThrowsNullReferenceException()
45+
public void DecodeThrowsNull()
1846
{
19-
Assert.Throws<NullReferenceException>(() => Base32Encoder.Encode(data: null));
47+
Assert.Throws<ArgumentNullException>(() => Base32Encoder.Decode(base32String: null));
2048
}
2149

2250
[Fact]
23-
public void WhenDataIsNullDecodeThrowsNullReferenceException()
51+
public void TryDecodeRejectsNull()
2452
{
25-
Assert.Throws<NullReferenceException>(() => Base32Encoder.Decode(base32String: null));
53+
string input = null;
54+
55+
Assert.False(input.TryDecodeBase32String(out var result));
2656
}
2757

2858
[Theory]
29-
[InlineData("a")]
30-
[InlineData("abc")]
31-
[InlineData("SEMTXET5UU6UZDD4AMK57TR46I==")]
32-
[InlineData("mjwgc===")]
33-
[InlineData("GEYq====")]
34-
public void WhenDataIsNotLegalBase32DecodeThrows(string input)
59+
[MemberData(nameof(InvalidBase32))]
60+
public void DecodeThrowsInvalidArgument(string input)
3561
{
3662
Assert.Throws<ArgumentException>(() => Base32Encoder.Decode(base32String: input));
3763
}
3864

3965
[Theory]
40-
[InlineData("", "")]
41-
[InlineData("f", "MY======")]
42-
[InlineData("fo", "MZXQ====")]
43-
[InlineData("foo", "MZXW6===")]
44-
[InlineData("foob", "MZXW6YQ=")]
45-
[InlineData("fooba", "MZXW6YTB")]
46-
[InlineData("foobar", "MZXW6YTBOI======")]
47-
public void WhenValidBase32StringProvideDecodeSucceeds(string decodedString, string base32String)
66+
[MemberData(nameof(InvalidBase32))]
67+
public void TryParseRejectsInvalidBase32(string input)
68+
{
69+
Assert.False(input.TryDecodeBase32String(out var result));
70+
}
71+
72+
[Theory]
73+
[MemberData(nameof(ValidBase32))]
74+
public void DecodesBase32(string decodedString, string base32String)
4875
{
4976
Assert.Equal(decodedString, Encoding.ASCII.GetString(base32String.FromBase32String()));
5077
}
5178

5279
[Theory]
53-
[InlineData("", "")]
54-
[InlineData("f", "MY======")]
55-
[InlineData("fo", "MZXQ====")]
56-
[InlineData("foo", "MZXW6===")]
57-
[InlineData("foob", "MZXW6YQ=")]
58-
[InlineData("fooba", "MZXW6YTB")]
59-
[InlineData("foobar", "MZXW6YTBOI======")]
60-
public void WhenDataIsEncodedTheResultIsCorrect(string input, string base32String)
80+
[MemberData(nameof(ValidBase32))]
81+
public void TryDecodesBase32(string decodedString, string base32String)
6182
{
62-
Assert.Equal(base32String, Base32Encoder.ToBase32String(Encoding.ASCII.GetBytes(input)));
83+
var success = base32String.TryDecodeBase32String(out var result);
84+
85+
Assert.True(success);
86+
Assert.Equal(decodedString, Encoding.ASCII.GetString(result));
6387
}
6488

89+
[Theory]
90+
[MemberData(nameof(ValidBase32))]
91+
public void EncodesBase32(string input, string base32String)
92+
{
93+
Assert.Equal(base32String, Base32Encoder.ToBase32String(Encoding.ASCII.GetBytes(input)));
94+
}
6595

6696
[Theory]
6797
[InlineData("")]
@@ -75,10 +105,13 @@ public void WhenDataIsEncodedItCanBeDecoded(string input)
75105

76106
// Act
77107
var encoded = byteArr.ToBase32String();
78-
var decoded = encoded.FromBase32String();
108+
var decoded1 = encoded.FromBase32String();
109+
var success = encoded.TryDecodeBase32String(out var decoded2);
79110

80111
// Assert
81-
Assert.True(byteArr.SequenceEqual(decoded));
112+
Assert.True(success);
113+
Assert.True(byteArr.SequenceEqual(decoded1));
114+
Assert.True(byteArr.SequenceEqual(decoded2));
82115
}
83116

84117
[Theory]
@@ -98,10 +131,13 @@ public void WhenPaddingIsRemovedItCanBeAppendedBack(int byteArrayLength)
98131

99132
// Act
100133
var withPadding = noPadding.AppendBase32Padding();
101-
var decoded = withPadding.FromBase32String();
134+
var decoded1 = withPadding.FromBase32String();
135+
var success = withPadding.TryDecodeBase32String(out var decoded2);
102136

103137
// Assert
104-
Assert.True(byteArr.SequenceEqual(decoded));
138+
Assert.True(success);
139+
Assert.True(byteArr.SequenceEqual(decoded1));
140+
Assert.True(byteArr.SequenceEqual(decoded2));
105141
}
106142
}
107143
}

0 commit comments

Comments
 (0)