Skip to content

Commit ceeff41

Browse files
author
Brent Schmaltz
committed
Merged PR 10199: Set MaximumDeflateSize
The Decompress method has been adjusted to only process a maximum number of chars. ---- #### AI-Generated Description The pull request adds support for limiting the size of decompressed JWT tokens to prevent decompression attacks. The main changes are: - Adding a `MaximumDeflateSize` property to various classes that handle compression and decompression, such as `DeflateCompressionProvider`, `CompressionProviderFactory`, and `JwtTokenDecryptionParameters`. - Passing the `MaximumDeflateSize` value to the `DecompressToken` method and checking if the decompressed token exceeds the limit before returning it. - Adding new unit tests and theory data to verify the functionality and handle different scenarios.
1 parent e986e22 commit ceeff41

File tree

9 files changed

+194
-10
lines changed

9 files changed

+194
-10
lines changed

src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,8 @@ private string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters val
10371037
new JwtTokenDecryptionParameters
10381038
{
10391039
DecompressionFunction = JwtTokenUtilities.DecompressToken,
1040-
Keys = keys
1040+
Keys = keys,
1041+
MaximumDeflateSize = MaximumTokenSizeInBytes
10411042
});
10421043
}
10431044

src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenDecryptionParameters.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal class JwtTokenDecryptionParameters
3939
/// <summary>
4040
/// Gets or sets the function used to attempt decompression with.
4141
/// </summary>
42-
public Func<byte[], string, string> DecompressionFunction { get; set; }
42+
public Func<byte[], string, int, string> DecompressionFunction { get; set; }
4343

4444
/// <summary>
4545
/// Gets or sets the encryption algorithm (Enc) of the token.
@@ -66,6 +66,15 @@ internal class JwtTokenDecryptionParameters
6666
/// </summary>
6767
public IEnumerable<SecurityKey> Keys { get; set; }
6868

69+
/// <summary>
70+
/// Gets and sets the maximum deflate size in chars that will be processed.
71+
/// </summary>
72+
public int MaximumDeflateSize
73+
{
74+
get;
75+
set;
76+
} = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
77+
6978
/// <summary>
7079
/// Gets or sets the 'value' of the 'zip' claim.
7180
/// </summary>

src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig
115115
/// </summary>
116116
/// <param name="tokenBytes"></param>
117117
/// <param name="algorithm"></param>
118+
/// <param name="maximumDeflateSize"></param>
118119
/// <exception cref="ArgumentNullException">if <paramref name="tokenBytes"/> is null.</exception>
119120
/// <exception cref="ArgumentNullException">if <paramref name="algorithm"/> is null.</exception>
120121
/// <exception cref="NotSupportedException">if the decompression <paramref name="algorithm"/> is not supported.</exception>
121122
/// <exception cref="SecurityTokenDecompressionFailedException">if decompression using <paramref name="algorithm"/> fails.</exception>
122123
/// <returns>Decompressed JWT token</returns>
123-
internal static string DecompressToken(byte[] tokenBytes, string algorithm)
124+
internal static string DecompressToken(byte[] tokenBytes, string algorithm, int maximumDeflateSize)
124125
{
125126
if (tokenBytes == null)
126127
throw LogHelper.LogArgumentNullException(nameof(tokenBytes));
@@ -131,7 +132,7 @@ internal static string DecompressToken(byte[] tokenBytes, string algorithm)
131132
if (!CompressionProviderFactory.Default.IsSupportedAlgorithm(algorithm))
132133
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10682, LogHelper.MarkAsNonPII(algorithm))));
133134

134-
var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm);
135+
var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm, maximumDeflateSize);
135136

136137
var decompressedBytes = compressionProvider.Decompress(tokenBytes);
137138

@@ -248,7 +249,7 @@ internal static string DecryptJwtToken(
248249
if (string.IsNullOrEmpty(zipAlgorithm))
249250
return Encoding.UTF8.GetString(decryptedTokenBytes);
250251

251-
return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm);
252+
return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm, decryptionParameters.MaximumDeflateSize);
252253
}
253254
catch (Exception ex)
254255
{

src/Microsoft.IdentityModel.Tokens/CompressionProviderFactory.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ private static bool IsSupportedCompressionAlgorithm(string algorithm)
7878
/// <param name="algorithm">the decompression algorithm.</param>
7979
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
8080
public ICompressionProvider CreateCompressionProvider(string algorithm)
81+
{
82+
return CreateCompressionProvider(algorithm, TokenValidationParameters.DefaultMaximumTokenSizeInBytes);
83+
}
84+
85+
/// <summary>
86+
/// Returns a <see cref="ICompressionProvider"/> for a specific algorithm.
87+
/// </summary>
88+
/// <param name="algorithm">the decompression algorithm.</param>
89+
/// <param name="maximumDeflateSize">the maximum deflate size in chars that will be processed.</param>
90+
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
91+
public ICompressionProvider CreateCompressionProvider(string algorithm, int maximumDeflateSize)
8192
{
8293
if (string.IsNullOrEmpty(algorithm))
8394
throw LogHelper.LogArgumentNullException(nameof(algorithm));
@@ -86,10 +97,11 @@ public ICompressionProvider CreateCompressionProvider(string algorithm)
8697
return CustomCompressionProvider;
8798

8899
if (algorithm.Equals(CompressionAlgorithms.Deflate))
89-
return new DeflateCompressionProvider();
100+
return new DeflateCompressionProvider { MaximumDeflateSize = maximumDeflateSize };
90101

91102
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10652, LogHelper.MarkAsNonPII(algorithm))));
92103
}
104+
93105
}
94106
}
95107

src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.IdentityModel.Logging;
55
using System;
6+
using System.Buffers;
67
using System.IO;
78
using System.IO.Compression;
89
using System.Text;
@@ -14,6 +15,8 @@ namespace Microsoft.IdentityModel.Tokens
1415
/// </summary>
1516
public class DeflateCompressionProvider : ICompressionProvider
1617
{
18+
private int _maximumTokenSizeInBytes = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
19+
1720
/// <summary>
1821
/// Initializes a new instance of the <see cref="DeflateCompressionProvider"/> class used to compress and decompress used the <see cref="CompressionAlgorithms.Deflate"/> algorithm.
1922
/// </summary>
@@ -41,6 +44,19 @@ public DeflateCompressionProvider(CompressionLevel compressionLevel)
4144
/// </summary>
4245
public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal;
4346

47+
/// <summary>
48+
/// Gets and sets the maximum deflate size in chars that will be processed.
49+
/// </summary>
50+
/// <exception cref="ArgumentOutOfRangeException">'value' less than 1.</exception>
51+
public int MaximumDeflateSize
52+
{
53+
get => _maximumTokenSizeInBytes;
54+
set => _maximumTokenSizeInBytes = (value < 1) ?
55+
throw LogHelper.LogExceptionMessage(
56+
new ArgumentOutOfRangeException(nameof(value),
57+
LogHelper.FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
58+
}
59+
4460
/// <summary>
4561
/// Decompress the value using DEFLATE algorithm.
4662
/// </summary>
@@ -51,16 +67,37 @@ public byte[] Decompress(byte[] value)
5167
if (value == null)
5268
throw LogHelper.LogArgumentNullException(nameof(value));
5369

54-
using (var inputStream = new MemoryStream(value))
70+
char[] chars = null;
71+
try
5572
{
56-
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
73+
chars = ArrayPool<char>.Shared.Rent(MaximumDeflateSize);
74+
using (var inputStream = new MemoryStream(value))
5775
{
58-
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
76+
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
5977
{
60-
return Encoding.UTF8.GetBytes(reader.ReadToEnd());
78+
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
79+
{
80+
// if there is one more char to read, then the token is too large.
81+
int bytesRead = reader.Read(chars, 0, MaximumDeflateSize);
82+
if (reader.Peek() != -1)
83+
{
84+
throw LogHelper.LogExceptionMessage(
85+
new SecurityTokenDecompressionFailedException(
86+
LogHelper.FormatInvariant(
87+
LogMessages.IDX10816,
88+
LogHelper.MarkAsNonPII(MaximumDeflateSize))));
89+
}
90+
91+
return Encoding.UTF8.GetBytes(chars, 0, bytesRead);
92+
}
6193
}
6294
}
6395
}
96+
finally
97+
{
98+
if (chars != null)
99+
ArrayPool<char>.Shared.Return(chars);
100+
}
64101
}
65102

66103
/// <summary>

src/Microsoft.IdentityModel.Tokens/LogMessages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ internal static class LogMessages
239239
public const string IDX10813 = "IDX10813: Unable to create a {0} from the properties found in the JsonWebKey: '{1}', Exception '{2}'.";
240240
public const string IDX10814 = "IDX10814: Unable to create a {0} from the properties found in the JsonWebKey: '{1}'. Missing: '{2}'.";
241241
public const string IDX10815 = "IDX10815: Depth of JSON: '{0}' exceeds max depth of '{1}'.";
242+
public const string IDX10816 = "IDX10816: Decompressing would result in a token with a size greater than allowed. Maximum size allowed: '{0}'.";
242243

243244
// Base64UrlEncoding
244245
public const string IDX10820 = "IDX10820: Invalid character found in Base64UrlEncoding. Character: '{0}', Encoding: '{1}'.";

src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,6 +1780,7 @@ protected string DecryptToken(JwtSecurityToken jwtToken, TokenValidationParamete
17801780
EncodedToken = jwtToken.RawData,
17811781
HeaderAsciiBytes = Encoding.ASCII.GetBytes(jwtToken.EncodedHeader),
17821782
InitializationVectorBytes = Base64UrlEncoder.DecodeBytes(jwtToken.RawInitializationVector),
1783+
MaximumDeflateSize = MaximumTokenSizeInBytes,
17831784
Keys = keys,
17841785
Zip = jwtToken.Header.Zip,
17851786
});

test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3579,6 +3579,66 @@ public static TheoryData<CreateTokenTheoryData> JWECompressionTheoryData
35793579
}
35803580
}
35813581

3582+
[Theory, MemberData(nameof(JweDecompressSizeTheoryData))]
3583+
public void JWEDecompressionSizeTest(JWEDecompressionTheoryData theoryData)
3584+
{
3585+
var context = TestUtilities.WriteHeader($"{this}.JWEDecompressionTest", theoryData);
3586+
3587+
try
3588+
{
3589+
var handler = new JsonWebTokenHandler();
3590+
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
3591+
var validationResult = handler.ValidateTokenAsync(theoryData.JWECompressionString, theoryData.ValidationParameters).Result;
3592+
theoryData.ExpectedException.ProcessException(validationResult.Exception, context);
3593+
}
3594+
catch (Exception ex)
3595+
{
3596+
theoryData.ExpectedException.ProcessException(ex, context);
3597+
}
3598+
3599+
TestUtilities.AssertFailIfErrors(context);
3600+
}
3601+
3602+
public static TheoryData<JWEDecompressionTheoryData> JweDecompressSizeTheoryData()
3603+
{
3604+
// The character 'U' compresses better because UUU in base 64, repeated characters compress best.
3605+
JsonWebTokenHandler jwth = new JsonWebTokenHandler();
3606+
SecurityKey key = new SymmetricSecurityKey(new byte[256 / 8]);
3607+
EncryptingCredentials encryptingCredentials = new EncryptingCredentials(key, "dir", "A128CBC-HS256");
3608+
TokenValidationParameters validationParameters = new TokenValidationParameters { TokenDecryptionKey = key };
3609+
3610+
TheoryData<JWEDecompressionTheoryData> theoryData = new TheoryData<JWEDecompressionTheoryData>();
3611+
3612+
string payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 40_000_000) });
3613+
string token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
3614+
theoryData.Add(new JWEDecompressionTheoryData
3615+
{
3616+
CompressionProviderFactory = new CompressionProviderFactory(),
3617+
ValidationParameters = validationParameters,
3618+
JWECompressionString = token,
3619+
TestId = "DeflateSizeExceeded",
3620+
ExpectedException = new ExpectedException(
3621+
typeof(SecurityTokenDecompressionFailedException),
3622+
"IDX10679:",
3623+
typeof(SecurityTokenDecompressionFailedException))
3624+
});
3625+
3626+
payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 50_000_000) });
3627+
token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
3628+
theoryData.Add(new JWEDecompressionTheoryData
3629+
{
3630+
CompressionProviderFactory = new CompressionProviderFactory(),
3631+
ValidationParameters = validationParameters,
3632+
JWECompressionString = token,
3633+
TestId = "TokenSizeExceeded",
3634+
ExpectedException = new ExpectedException(
3635+
typeof(ArgumentException),
3636+
"IDX10209:")
3637+
});
3638+
3639+
return theoryData;
3640+
}
3641+
35823642
[Theory, MemberData(nameof(JWEDecompressionTheoryData))]
35833643
public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
35843644
{

test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Linq;
77
using System.Security.Claims;
8+
using System.Threading.Tasks;
89
using Microsoft.IdentityModel.JsonWebTokens;
910
using Microsoft.IdentityModel.Protocols;
1011
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
@@ -1116,6 +1117,67 @@ public void InstanceClaimMappingAndFiltering()
11161117
Assert.True(identity.HasClaim("internalClaim", "claimValue"));
11171118
}
11181119

1120+
[Theory, MemberData(nameof(JweDecompressSizeTheoryData))]
1121+
public async Task JWEDecompressionSizeTest(JWEDecompressionTheoryData theoryData)
1122+
{
1123+
var context = TestUtilities.WriteHeader($"{this}.JWEDecompressionTest", theoryData);
1124+
1125+
try
1126+
{
1127+
var handler = new JwtSecurityTokenHandler();
1128+
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
1129+
var validationResult = await handler.ValidateTokenAsync(theoryData.JWECompressionString, theoryData.ValidationParameters).ConfigureAwait(false);
1130+
theoryData.ExpectedException.ProcessException(validationResult.Exception, context);
1131+
}
1132+
catch (Exception ex)
1133+
{
1134+
theoryData.ExpectedException.ProcessException(ex, context);
1135+
}
1136+
1137+
TestUtilities.AssertFailIfErrors(context);
1138+
}
1139+
1140+
public static TheoryData<JWEDecompressionTheoryData> JweDecompressSizeTheoryData()
1141+
{
1142+
// The character 'U' compresses better because UUU in base 64 is VVVV and repeated characters compress best
1143+
1144+
JsonWebTokenHandler jwth = new JsonWebTokenHandler();
1145+
SecurityKey key = new SymmetricSecurityKey(new byte[256 / 8]);
1146+
EncryptingCredentials encryptingCredentials = new EncryptingCredentials(key, "dir", "A128CBC-HS256");
1147+
TokenValidationParameters validationParameters = new TokenValidationParameters { TokenDecryptionKey = key };
1148+
1149+
TheoryData<JWEDecompressionTheoryData> theoryData = new TheoryData<JWEDecompressionTheoryData>();
1150+
1151+
string payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 40_000_000) });
1152+
string token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
1153+
theoryData.Add(new JWEDecompressionTheoryData
1154+
{
1155+
CompressionProviderFactory = new CompressionProviderFactory(),
1156+
ValidationParameters = validationParameters,
1157+
JWECompressionString = token,
1158+
TestId = "DeflateSizeExceeded",
1159+
ExpectedException = new ExpectedException(
1160+
typeof(SecurityTokenDecompressionFailedException),
1161+
"IDX10679:",
1162+
typeof(SecurityTokenDecompressionFailedException))
1163+
});
1164+
1165+
payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 50_000_000) });
1166+
token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
1167+
theoryData.Add(new JWEDecompressionTheoryData
1168+
{
1169+
CompressionProviderFactory = new CompressionProviderFactory(),
1170+
ValidationParameters = validationParameters,
1171+
JWECompressionString = token,
1172+
TestId = "TokenSizeExceeded",
1173+
ExpectedException = new ExpectedException(
1174+
typeof(ArgumentException),
1175+
"IDX10209:")
1176+
});
1177+
1178+
return theoryData;
1179+
}
1180+
11191181
[Theory, MemberData(nameof(JWEDecompressionTheoryData))]
11201182
public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
11211183
{

0 commit comments

Comments
 (0)